-
Notifications
You must be signed in to change notification settings - Fork 21.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow users to customize ActiveStorage controllers without having to override them #41505
base: main
Are you sure you want to change the base?
Conversation
Another option would be to have # frozen_string_literal: true
# The base class for all Active Storage controllers.
# You should implement authentication and authorization mechanisms appropriate to your needs.
# For example, for the `create` and `update` actions you can authenticate the user to prevent distributed attacks
# and for `show` action you can authenticate the user + authorize access to the specific `ActiveStorage::Blob` according to your business logic.
class ActiveStorage::BaseController < ActionController::Base
include ActiveStorage::SetCurrent, ActiveStorage::Streaming
protect_from_forgery with: :exception
self.etag_with_template_digest = false
end idk.. there are many possible solutions but I'd love to hear your thoughts @georgeclaghorn because I think this is important, and I'd love to help making ActiveStorage a more secure place. |
I’m not sure about this specific approach yet—need to think about it more. This is a good start, though, and I’d like to do something along these lines. Another thing to consider are these authorization monkey-patches we use (cleaned up a bit for sharing), which I’d love to upstream in some form: ActiveSupport.on_load :active_storage_blob do
def accessible_to?(accessor)
attachments.includes(:record).any? { |attachment| attachment.accessible_to?(accessor) } || attachments.none?
end
end
ActiveSupport.on_load :active_storage_attachment do
def accessible_to?(accessor)
record.try(:accessible_to?, accessor)
end
end
ActiveSupport.on_load :action_text_rich_text do
def accessible_to?(accessor)
record.try(:accessible_to?, accessor)
end
end
module ActiveStorage::Authorize
extend ActiveSupport::Concern
included do
before_action :require_authorization
end
private
def require_authorization
head :forbidden unless authorized?
end
def authorized?
@blob.accessible_to?(Current.identity)
end
end
Rails.application.config.to_prepare do
ActiveStorage::Blobs::RedirectController.include ActiveStorage::Authorize
ActiveStorage::Blobs::ProxyController.include ActiveStorage::Authorize
ActiveStorage::Representations::RedirectController.include ActiveStorage::Authorize
ActiveStorage::Representations::ProxyController.include ActiveStorage::Authorize
end Application models implement class Entry < ApplicationRecord
def accessible_to?(accessor)
in? accessor.accessible_entries
end
end |
Glad to hear you are onboard with this 😄 I'll continue thinking on alternatives and analyze their pros and cons. Regarding authorization, I think it's fair to provide out of the box a mechanism just for ActiveStorage. Specifically talking about the code you shared, I like it a lot but the only doubt I have is if the class User < ApplicationRecord
has_one_attached :passport
has_one_attached :avatar
def accessible_to?(accessor, attachment_name)
return true if attachment_name == "avatar" # avatar is publicly accessible
in? accessor.accessible_entries
end
end thoughts? |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
🦄 |
@santib What's left here? I'm happy to help move this along if I can. |
❤️ That'd be amazing! Quoting George:
So I think we need to discuss what's the best approach for this. Some options:
All of them have their pros and cons but for now I'm inclined to the current PR approach since it doesn't affect anyone but allows further customization. Once we've decided which approach to follow we can polish the implementation and write some docs to explain users how to add authorization on ActiveStorage |
My initial thought was partial to Customizing that base controller means the user can opt-out completely and diverge from AS::Base which may create bigger gaps making upgrades even more difficult. While providing a generator means, there is still a common base (unless ofcourse the user changes it) allowing greater control between upgrades without diverging too far. For example, the user could generate a new controller and start migrating their old controller there -- but if they're monkey-patching the crap out of AS::Base it will make our job more difficult, possibly. Just some random thoughts here, sorry if it's kind of a mess. Anyways, I'm not sure who's best to make this decision -- I'd like to consider what other libraries are doing first but perhaps @pixeltrix or @carlosantoniodasilva knows? |
Still having a think about this one but #32238 is relevant here - wondering whether having a The pattern of inheriting from a configuration is certainly one that's seen in the wild - as the original comment says, it's what Devise uses but it's not a pattern we've used in Rails before. |
What about making @georgeclaghorn's monkey patches a commented-out initializer? |
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
I'm partial to the solution originally proposed. It follows a pattern already established in Rails. For example, using an application-specific I'm not saying its a better solution that what @georgeclaghorn provided, but it seems to fit the Rails approach in other areas. 🤷♂️ |
This is a good idea! Can we remove the Having a Which approach does the Rails core team prefer? |
It seems nowadays the recommended way to ask whether a PR could be revived is from Discord (there should be a channel for that). |
I don't understand why it should be so hard to keep issues open / reopen them. That's just going to cause people to open a duplicate issue/PR — or (if they notice in time) cause people to add extra "not stale" noise when the bot warns it's about to be closed. Wouldn't it be preferable to keep the discussion together in one place instead of spreading across duplicate issues? (Similarly, moving the meta conversation about an issue out to a completely separate system (Discord) seems like the wrong direction, because it wouldn't be visible to/discoverable by those arriving at the closed issue.) I get how it's useful to have stale issues not cluttering the list. But if interes/activity later picks up again, then "stale" is no longer accurate and its status should be automatically updated to reflect its newfound freshness... like it did back here: |
Thinking more about the What would make it less confusing is if you had to explicitly configure it to use your class as a base class, like In the meantime, given that the docs-recommended approach has the problems listed by the OP (it's a do-it-all-yourself copy-paste solution that doesn't benefit from the default routes provided by ActiveStorage or from improvements made upstream), the only reasonable option seems to be to monkey patch... and @georgeclaghorn's approach seems like a really clean way to do that — thanks for that! |
Hey @TylerRick I feel you, but don't worry, this has already been addressed by the Rails Core team https://rubyonrails.org/2022/6/13/rails-discord-server-is-now-open-to-the-public We can ask them in the discord to continue the conversation here 😄 |
Exactly, PRs no longer go stale for this reason. I'll re-open so it hopefully gets more attention |
Summary
Related to #41503 , it should be normal for production apps to add authentication and authorization to their ActiveStorage controllers. Unfortunately, there are 2 possible ways to achieve it currently:
None of them is ideal because in the end you can't benefit from Rails upgrades (bug fixes, etc) so the intention of this PR is to let people define a parent controller (inspired by Devise, maybe @carlosantoniodasilva can tell us his experience on this feature) so that people can add authentication and authorization in a single place and still benefit from the default controllers.
Usage example:
If there is interest in a feature like this I can add tests, add it to the guides and CHANGELOG. Please let me know