In my project my authorization policy required to check multiple conditions for a single action and hence required contextual error messages to be raised. But because of Pundit::NotAuthorizedError binded to query option I had to use a work-around to raise such contextual error messages.
Please refer the code below on how I made it work:
/config/locales/en.yml
en:
pundit:
user_policy:
show?: 'You cannot view this user!'
create?: 'You cannot create users!'
cannot_create_for_other_provider: 'You cannot create user for selected provider!'
/app/policies/custom_application_policy.rb
class CustomApplicationPolicy < ApplicationPolicy
attr_accessor :policy_error_message
def initialize(user, record)
super(user, record)
@policy_error_message = nil
end
def policy_name
self.class.to_s.underscore
end
def policy_error_message!(error_key=nil, &block)
error_key = block.call if block_given?
if error_key.present?
@policy_error_message = I18n.t("#{policy_name}.#{error_key}", scope: "pundit", default: :default)
end
end
end
/app/policies/user_policy.rb
class UserPolicy < CustomApplicationPolicy
def create?
if !admin_user? && !provider_administrator?
return false
end
if provider_administrator?
unless users_provider_matches?
policy_error_message!('cannot_create_for_other_provider')
return false
end
end
true
end
private
def users_provider_matches?
return false unless user_provider.present?
user_provider_id == record.provider_id
end
end
/app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError do |exception|
policy = exception.policy
if policy.respond_to?(:policy_error_message) && policy.policy_error_message.present?
message = policy.policy_error_message
else
policy_name = exception.policy.class.to_s.underscore
query = exception.query
message = t("#{policy_name}.#{query}", scope: "pundit", default: :default)
end
flash[:error] = message
redirect_to(request.referrer || root_path)
end
It would be really a great thing if this workaround can be addressed in a more structured manner in the Gem itself without requiring the end-users like me to use some work-around like above.
Also I must say this: Pundit is a beautiful gem I have discovered to take care of implementing authorization policies in simplest manner and also open for extension. Before finding I was using CanCan but it didn't provided the simplicity and flexibility I was seeking and hence I started hunting for an alternative and I ended up finding Pundit. Am really impressed with its simplistic design. And the Gem code also is easy enough to understand.
Thanks to its author as well as the contributors who have brought it to the stage it is currently at.
A way to do this without changing pundit or building your own solution in policies/controllers would be to break up your policy query into one for each different message. You would have more #authorize calls in the controller, but this is likely not distasteful, and they can be pulled out into private methods. You would be able to use this approach for messaging without changing anything: https://github.com/varvet/pundit#creating-custom-error-messages
class ThingsController < ApplicationController
def create
authorize_create
end
private
def authorize_create
authorize @thing, :query1
authorize @thing, :query2
authorize @thing, :query3
end
end
@jiggneshhgohel - Did you solved it by yourself?
Currently, I am using @johnam solution, but if the code gets more complex it looses on its beauty.
@Kani999 the solution I used is what I have posted in foremost comment in this issue. And I have not pursued this further for a permanent solution. But as I said what I employed I consider it as a sort of work-around and some modifications should be made in the gem itself to tackle this in structured manner.
@jiggneshhgohel - Maybe I give it a try when my project grow. Thank's for the working solution, I've missed this information in your post.
Something like this would achieve what you want, I think.
class CustomApplicationPolicy < ApplicationPolicy
attr_accessor :policy_error_message
def initialize(user, record)
super(user, record)
@policy_error_key = nil
end
end
class UserPolicy < CustomApplicationPolicy
def create?
unless users_provider_matches?
self.policy_error_key = :cannot_create_for_other_provider
return false
end
true
end
end
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError do |exception|
policy = exception.policy
policy_name = exception.policy.class.to_s.underscore
error_key = if policy.respond_to?(:policy_error_key) && policy.policy_error_key
policy.policy_error_message
else
exception.query
end
flash[:error] = t("#{policy_name}.#{error_key}", scope: "pundit", default: :default)
redirect_to(request.referrer || root_path)
end
end
I haven't tested the above code though :) I think it's easy enough for end users to add if needed so I don't think I will add it to Pundit at the moment.
Linus, great idea. But there are some errors still in your code. Let me post how I implemented it.
Extend the application_policy.rb attr_reader with :error_message_key
class ApplicationPolicy
attr_reader :user, :record, :error_message_key
...
end
Define the custom error message in your policy your are checking your action against
class UserPolicy < CustomApplicationPolicy
def create?
unless users_provider_matches?
@error_message_key = :not_allowed_to_edit_admin_user
return false
end
true
end
end
Rescue from the Exception in the application_controller.rb
class ApplicationController < ActionController::Base
include Pundit
rescue_from Pundit::NotAuthorizedError do |exception|
policy = exception.policy
policy_name = policy.class.to_s.underscore
error_key = policy.error_message_key ? policy.error_message_key : exception.query
flash[:error] = t "#{policy_name}.#{error_key}", scope: "pundit", default: :default
redirect_to(request.referrer || root_path)
end
end
Most helpful comment
Linus, great idea. But there are some errors still in your code. Let me post how I implemented it.
Extend the application_policy.rb attr_reader with :error_message_key
Define the custom error message in your policy your are checking your action against
Rescue from the Exception in the application_controller.rb