When Paperclip attachments should only be downloadable for selected users, there are three ways to go.
The same applies to files in Carrierwave.
- Deliver attachments through Rails
The first way is to store Paperclip attachments not in the default public/system
, but in a private path like storage
inside the current release. You should prefer this method when dealing with sensitive data.
Make sure that folder is linked to the current release when deploying:
-
On our OpsComplete Show archive.org snapshot deployments, you only need to add
storage
to thelinked_dirs
inconfig/deploy.rb
-
For Capistrano 2, add this to your
config/deploy.rb
:namespace :paperclip do desc "Create a storage folder for Paperclip attachment in shared path" task :create_storage do run "mkdir -p #{shared_path}/storage" end desc "Link the Paperclip storage folder into the current release" task :link_storage do run "ln -nfs #{shared_path}/storage #{release_path}/storage" end end before "deploy:setup", 'paperclip:create_storage' after "deploy:update_code", "paperclip:link_storage"
-
For Capistrano 3, add the following (untested) to your
config/deploy.rb
:namespace :paperclip do desc "Create a storage folder for Paperclip attachment in shared path" task :create_storage do run "mkdir -p #{shared_path}/storage" end end append :linked_dirs, 'storage' before "deploy:updating", 'paperclip:create_storage'
Also make sure that storage
is ignored in your .gitignore
or mayhem will ensue.
Now write a controller action that sends the attachment to authorized users. That action should be protected by
Consul
Show archive.org snapshot
or a before_action
.
class NotesController
power :notes # Authorization through Consul
def attachment
note = Note.find(params[:id])
send_file note.attachment.path
end
end
Connect the action in your config/routes.rb
:
map.resources :notes, :member => { :attachment => :get }
Your attachment can be configured like this:
class Note < ActiveRecord::Base
has_attached_file :attachment, :path => ":rails_root/storage/:rails_env/attachments/:id/:style/:basename.:extension"
end
To link to the protecting controller actions, e.g. in a view:
link_to 'Download attachment', [:attachment, @note]
- Store attachments in hashed paths
The method above is very secure, but it hits the framework stack everytime an attachment is downloaded. E.g. when you display an image gallery with 40 thumbnails, that would generate 40 expensive Rails requests.
An alternative is to do store Paperclip attachments in the default public/system
, but generate the path from a hash function, e.g. attachments/519b6d87b1225ba3/9/beach.jpg
. The hash function hashes the containing record id, class name and a secret that must be unique to every application you deploy. This way attachments will only be accessible to users who know the link. However, you can never take authorization away (rarely an issue with images) and if you have directory indexes activated in Apache, you screwed up big time.
Copy the code below to config/initializers/paperclip_hashed_path.rb
:
Paperclip.interpolates :hashed_path do |attachment, style|
secret = 'CHANGE-THIS-FOR-EVERY-APPLICATION! jivEutIgtepsIvgikUpCoshBypMowjuegbisIgJo'
hash = Digest::SHA512.hexdigest("--#{Rails.env}--#{attachment.class.name}--#{attachment.instance.id}--#{mounted_as}--#{secret}--")
# 4 bit per character, so 128 bit for the full hash. Check if still appropriate!
hash.slice(0, 32)
end
Remember to change the secret in line 2 for every application you deploy! Changing a duplicated secret is very painful. We have a note on how to create strong secrets.
You can now use the hashed_path
interpolation in your Paperclip attachments:
class Note < ActiveRecord::Base
attachment_virtual_path = "/system/attachments/:rails_env/:hashed_path/:id/:style/:basename.:extension"
attachment_real_path = ":rails_root/public" + attachment_virtual_path
has_attached_file :attachment, :path => attachment_real_path, :url => attachment_virtual_path
end
Paperclip will now automatically store attachments using hashed paths, and generate URLs accordingly. You don't need a protecting controller action here, because all security lies in the hashed secret.
To link to the protecting controller actions, e.g. in a view:
link_to 'Download attachment', [:attachment, @note]
After you deploy, make sure you do not get a directory index when you access http://yourpage.com/system/attachments
.
You can configure your apache server to expire file-URLs after some time, so they are only available as long as you want to provide it to the user. This gives some additional security to the not-that-incredibly-secure hashed paths storage approach. Read here for details.
- Further reading