Pundit: Making Pundit::NotAuthorizedError flexible to accept custom keys instead of binding it to just `query`

Created on 9 Mar 2018  路  6Comments  路  Source: varvet/pundit

  • Ubuntu 12.04
  • Ruby 2.3.2
  • Rails 5.0.0.1
  • Pundit 1.1.0

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.

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

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

All 6 comments

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
Was this page helpful?
0 / 5 - 0 ratings

Related issues

epinault picture epinault  路  6Comments

dowi picture dowi  路  4Comments

AngelVillanueva picture AngelVillanueva  路  3Comments

richjdsmith picture richjdsmith  路  4Comments

mattruzicka picture mattruzicka  路  5Comments