-
Notifications
You must be signed in to change notification settings - Fork 634
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
Support custom error messages in policies #655
Conversation
This commit allows policies to define the error messages set on `Pundit::NotAuthorizedError` exceptions when `#authorize` fails. The rationale is described in detail in GitHub issue varvet#654[0], and summarized below. Some queries can fail in multiple ways; for instance, class PostPolicy def update? if record.author != user ... # failure case 1 elsif record.archived ... # failure case 2 end true end end In their controllers, users might wish to handle different failure modes in different ways, but prior to this commit, there was only one way to tell the difference— namely, by raising errors inside the query method: def update? if record.author != user raise Pundit::NotAuthorizedError, 'You’re not the author!' elsif record.archived raise Pundit::NotAuthorizedError, 'This post is old news!' end true end This breaks the expectation that query methods should return booleans, which in turn breaks a pattern for using query methods in views: <% if policy(@post).update? %> <%= link_to "Edit post", edit_post_path(@post) %> <% end %> 973b63b added a `reason` option to the NotAuthorizedError initializer, but ultimatly required the same approach of raising errors in queries. --- This commit enables a cleaner method of passing a custom error message to exceptions from within policies, without violating the expectations of where exceptions are raised from. class PostPolicy attr_accessor :error_message def update? self.error_message = if record.author != user 'You’re not the author!' elsif record.archived 'This post is old news!' end error_message.nil? end end [0]: varvet#654
c6c4f3e
to
a129293
Compare
👍 for this I think this approach is better than the current one included in master branch - 973b63b |
Maybe the policy method could just return a failure reason or a custom query symbol? Generating error messages should be handled at the presentation layer. |
Can you elaborate? Applications with no presentation layer at all (e.g., CLI apps) raise errors and set messages on them all the time; why should it be the sole province of the presentation layer in a web app? The presentation layer can decide what is shown to the user, and as the developer, you can decide whether that is |
I'm not saying it needs to be on the controller level, API-only applications have presentation layers as well. In a web application with a frontend it can be in a Presenter or a Decorator, in API applications it can be in the serializer and/or entity object. Bubbling up messages along with the error is fine as well. As you said, some applications might not actually have a presentation layer. The library should be able to send a failure reason or an error message and leave the decision about how error messages are displayed to the application. |
100% agree, but I also don't see why the error message displayed by the application has to be the same as the value of def foo
authorize(@post)
rescue Pundit::NotAuthorizedError => e
case e.message
when 'not author'
# display one error message...
when 'archived
# display another error message...
end
end Maybe I am misunderstanding your original question:
Can you provide an example of how this would be implemented, so I can see what you're talking aobut? |
Your example is almost exactly what I had in mind The I figured the policy method could return a symbol that the error can be initialized with. Policy def foo?
:no_user unless user
:archived if record.archived?
true
end Application Code def foo
authorize(@post)
rescue Pundit::NotAuthorizedError => e
case e.reason
when :not_author
# display one error message...
when :archived
# display another error message...
end
end |
Interesting! I assume you actually meant the following: def foo?
return :no_user unless user
return :archived if record.archived?
true
end because without The problem with this is that raise NotAuthorizedError, ... unless policy.public_send(query) and symbols (like This can be resolved by setting an attribute on the policy instead, which def foo?
self.reason = :no_user unless user
self.reason = :archived if record.archived?
reason.nil?
end which is basically what my original PR proposes, but with I don't know how the Pundit authors feel about adding even more new attributes to the policy class; personally, I think |
One way to deal with custom failures is right here, I use it a lot in my code. My comment is barely relevant though, just an idea, don't mind me. :) |
Me and @dgmstuart had a chat about this, and I believe we both agreed that query methods should be encouraged to return a boolean (as opposed to raising an exception). A possible problem with the approach suggested in this PR is that we've got memoized policy instances: Lines 256 to 263 in 2714875
If I'm not wrong this means that the error reason here could become very confusing in case you query the same policy multiple times, e.g. in a controller action: def do_something_in_a_controller
if policy(record).can_do_this? # .error_message is now set
# …
elsif policy(record).can_do_that? # if this is true, .error_message is still going to be set from previous statement due to memoized instance
# …
else
render policy(record).error_message # error message from _second_ query, the first one is lost
end
end I haven't verified this in actual code/test-case yet. |
Revisiting this today, and I don't think that having stateful policies as the default recommended approach is a good idea. Our main state keeping today is the policy/scope cache, and that's about it. If it works for you, that's great, keep doing it! I'm mainly mindful of recommending it for all. There's an approach posted in #654 that has a slightly different approach: #654 (comment) I'd still love to hear how y'all end up solving this problem in your apps. Keep that in #654 though! |
This commit allows policies to define the error messages set on
Pundit::NotAuthorizedError
exceptions when#authorize
fails. The rationale is described in detail in GitHub issue #654, and summarized below.Some queries can fail in multiple ways; for instance,
In their controllers, users might wish to handle different failure modes in different ways, but prior to this commit, there was only one way to tell the difference—namely, by raising errors inside the query method:
This breaks the expectation that query methods should return booleans, which in turn breaks a pattern for using query methods in views:
973b63b added a
reason
option to the NotAuthorizedError initializer, but ultimately required the same approach of raising errors in queries.This commit enables a cleaner method of passing a custom error message to exceptions from within policies, without violating the expectations of where exceptions are raised from.