Search code examples
ruby-on-railspunditrefile

Pundit authorizaton for files uploaded with Refile gem


How would I do authorization on files uploaded with Refile gem using Pundit? I have uploaded files which should be restricted to the user that uploaded them, but anyone with the url that Refile's attachment_url generates can access the file. Since Refile uses it's own Sinatra app, there's no rails controller for me to call Pundit's authorize method in.


Solution

  • In your controller, you can have a method which will download the file. For a more complicated example, let's say that you have a download action in your UsersController controller. From within here, you can use pundit as you normally would. This download action grabs the image of the user.

    Disclaimer: This is a horrible example of production practice as you are going to be hammering the server if this action is called a lot. You are essentially resizing the image every single time this action is called. However, as a proof of concept to go outside of how refile file download normally works and adds in the authorization you're seeking.

    We create a processor variable which initializes the ImageProcessor fill option. We then create a temporary file and set it to binary mode. We take the file from our user model and read it into the temporary file. Rewind the temporary file to the beginning of the file and read it into MiniMagick. We then call on our processor to convert the temporary file (leaving the original intact). We then send the file to the user.

      def download
        @user = User.find(params[:id])
        authorize @user
        processor = Refile.processor(:fill, Refile::ImageProcessor.new(:fill))
        temp_file = Tempfile.new('profile_image')
        temp_file.binmode
        temp_file.write @user.profile_image.read
        temp_file.rewind
        image_file = MiniMagick::Image.new(temp_file.path)
        file = processor.fill(image_file, 150, 150)
        temp_file.close
        send_file file.path
      end  
    

    Here is the example of it rendering the file as an image_tag

    enter image description here

    Along with the code that calls the image to be resized and downloaded.

    <%= image_tag user_download_path(@user), class: 'img-circle img-thumbnail' %>
    

    You can set your action to accept various other processing options and dimensions. For this example, I'm showing how to fill a size of 150x150 pixels.

    Edit to add more clarity:

    The function of the temp_file is to leave the original image alone. If you are looking to simply provide an unprocessed download of the original file, you could do something like this below. You should also read on send_file and send_data as they provide other things like filename, disposition, content_type, etc. options for customizing the download and how it should be handled.

      def download
        @user = User.find(params[:id])
        authorize @user
        send_file @user.profile_image.download
      end
    

    Edit: I looked further into the Refile source and found that the creation of the file links is caused by the mounting of the engine within the routes. Create an initializer file and place the code below in there. This will allow you to keep the existing functionality described above while removing the public links to the files uploaded.

    Refile.configure do |config|
      # config.direct_upload = ["cache"]
      # config.allow_origin = "*"
      # config.logger = Logger.new(STDOUT)
      # config.mount_point = "attachments"
      config.automount = false
      # config.content_max_age = 60 * 60 * 24 * 365
      # config.types[:image] = Refile::Type.new(:image, content_type: %w[image/jpeg image/gif image/png])
    end