diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 0b95461498e16..ece41661ff93a 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -786,14 +786,34 @@ All Active Storage controllers are publicly accessible by default. The generated URLs use a plain [`signed_id`][ActiveStorage::Blob#signed_id], making them hard to guess but permanent. Anyone that knows the blob URL will be able to access it, even if a `before_action` in your `ApplicationController` would otherwise -require a login. If your files require a higher level of protection, you can -implement your own authenticated controllers, based on the -[`ActiveStorage::Blobs::RedirectController`][], -[`ActiveStorage::Blobs::ProxyController`][], -[`ActiveStorage::Representations::RedirectController`][] and -[`ActiveStorage::Representations::ProxyController`][] +require a login. -To only allow an account to access their own logo you could do the following: +If you need to, there's a few ways to make this more secure. + +#### Make URLs expire by default + +By default, `config.active_storage.urls_expire_in` is `nil`. If you give it a value, +then URLs will not be permanent. This works for all URLs generated by Active Storage, +when using `link_to`, `url_for`, `image_tag`, and so on + +```ruby +# config/application.rb +config.active_storage.urls_expire_in = 1.day +``` + +In this example, the URL will work when accessed immediately, but a day later, will stop working. +Calling `url_for` again will return a different URL. + +```ruby +url_for(user.avatar) +# => https://www.example.com/rails/active_storage/blobs/redirect/:signed_id/my-avatar.png +``` + +#### Create dedicated routes for specific actions + +You can make dedicated routes for application functionality, and add your own +authentication logic to these actions. For example, say you want to display the +current authenticated account's logo on every page. ```ruby # config/routes.rb @@ -805,9 +825,6 @@ end ```ruby # app/controllers/logos_controller.rb class LogosController < ApplicationController - # Through ApplicationController: - # include Authenticate, SetCurrentAccount - def show redirect_to Current.account.logo.url end @@ -818,18 +835,82 @@ end <%= image_tag account_logo_path %> ``` -And then you should disable the Active Storage default routes with: +In `ApplicationController` you would run authentication logic, so users that are +not logged in are redirected away, and the current account is set. + +#### Implement your own Active Storage controllers + +By overriding `resolve_model_to_route`, you can provide your own routing method that +will take an Active Storage model and generate the appropriate route for it. Then you can +create your own routes, pointing to your own controllers. See [`activestorage/config/routes.rb`][routes.rb] +for the full set of routes that Rails generates. + +```ruby +# config/application.rb +config.active_storage.resolve_model_to_route = :active_storage_authenticated_director +``` + +```ruby +# config/routes.rb + get "/storage/blobs/:signed_id/*filename" => "storage/blobs#show", as: :active_storage_authenticated_blob + get "/representations/redirect/:signed_blob_id/:variation_key/*filename" => "storage/representations_redirect#show", as: :active_storage_authenticated_representation + direct :active_storage_authenticated_director do |model, options| + expires_in = options.delete(:expires_in) { ActiveStorage.urls_expire_in } + expires_at = options.delete(:expires_at) + if model.respond_to?(:signed_id) + route_for( + :active_storage_authenticated_blob, + model.signed_id(expires_in: expires_in, expires_at: expires_at), + model.filename, + options + ) + else + route_for( + :active_storage_authenticated_representation, + model.blob.signed_id(expires_in: expires_in, expires_at: expires_at), + model.variation.key, + model.blob.filename, + options + ) + end + end +``` + +This controller copies the implementation of `ActiveStorage::Blobs::RedirectController`, but +as it subclasses our `ApplicationController` it will include authentication checks. You could +take this even further by running user-specific permission checks based on the requested blob. ```ruby +# app/controllers/storage/blobs_controller.rb +class Storage::BlobsController < ApplicationController + include ActiveStorage::SetBlob + include ActiveStorage::SetCurrent + protect_from_forgery with: :exception + + def show + expires_in(ActiveStorage.service_urls_expire_in) + redirect_to @blob.url(disposition: params[:disposition]), allow_other_host: true + end +end +``` + +With this approach, if you call `url_for(attachment)` or `image_tag(attachment.representation)`, +`active_storage_authenticated_director` will get used to generate a URL that will pass through your +secure `Storage::BlobsController`. + + +#### Disable Active Storage routes + +To disable Active Storage routes entirely: + +```ruby +# config/application.rb config.active_storage.draw_routes = false ``` -to prevent files being accessed with the publicly accessible URLs. +To link to an attachment or render a file you will need to generate the URL by hand. -[`ActiveStorage::Blobs::RedirectController`]: https://api.rubyonrails.org/classes/ActiveStorage/Blobs/RedirectController.html -[`ActiveStorage::Blobs::ProxyController`]: https://api.rubyonrails.org/classes/ActiveStorage/Blobs/ProxyController.html -[`ActiveStorage::Representations::RedirectController`]: https://api.rubyonrails.org/classes/ActiveStorage/Representations/RedirectController.html -[`ActiveStorage::Representations::ProxyController`]: https://api.rubyonrails.org/classes/ActiveStorage/Representations/ProxyController.html +[routes.rb]: https://github.com/rails/rails/blob/e0a9ef12b39a4871d8f143dae926931b3c3a0919/activestorage/config/routes.rb Downloading Files -----------------