module ActionPolicy::Policy::FailureReasons::Reasons

Provides failure reasons tracking functionality. That allows you to distinguish between the reasons why authorization was rejected.

It's helpful when you compose policies (i.e. use one policy within another).

For example:

class ApplicantPolicy < ApplicationPolicy
  def show?
    user.has_permission?(:view_applicants) &&
      allowed_to?(:show?, object.stage)
  end
end

Now when you receive an exception, you have a reasons object, which contains additional information about the failure:

rescue_from ActionPolicy::Unauthorized do |ex|
  ex.policy #=> ApplicantPolicy
  ex.rule #=> :show?
  ex.result.reasons.details  #=> {stage: [:show?]}
end

NOTE: the reason key (`stage`) is a policy identifier (underscored class name by default). For namespaced policies it has a form of:

class Admin::UserPolicy < ApplicationPolicy
  # ..
end

reasons.details #=> {:"admin/user" => [:show?]}

You can also wrap local rules into `allowed_to?` to populate reasons:

class ApplicantPolicy < ApplicationPolicy
  def show?
    allowed_to?(:view_applicants?) &&
      allowed_to?(:show?, object.stage)
  end

  def view_applicants?
    user.has_permission?(:view_applicants)
  end
end

NOTE: there is `check?` alias for `allowed_to?`.

You can provide additional details to your failure reasons by using a `details: { … }` option:

class ApplicantPolicy < ApplicationPolicy
  def show?
    allowed_to?(:show?, object.stage)
  end
end

class StagePolicy < ApplicationPolicy
  def show?
    # Add stage title to the failure reason (if any)
    # (could be used by client to show more descriptive message)
    details[:title] = record.title

    # then perform the checks
    user.stages.where(id: record.id).exists?
  end
end

# when accessing the reasons
p ex.result.reasons.details #=> { stage: [{show?: {title: "Onboarding"}] }

NOTE: when using detailed reasons, the `details` array contains as the last element a hash with ALL details reasons for the policy (in a form of <rule> => <details>).

Public Class Methods

included(base) click to toggle source
# File lib/action_policy/policy/reasons.rb, line 189
def included(base)
  base.result_class.prepend(ResultFailureReasons)
end

Public Instance Methods

allowed_to?(rule, record = :__undef__, inline_reasons: false, **options) click to toggle source
# File lib/action_policy/policy/reasons.rb, line 199
def allowed_to?(rule, record = :__undef__, inline_reasons: false, **options)
  res =
    if (record == :__undef__ || record == self.record) && options.empty?
      rule = resolve_rule(rule)
      policy = self
      with_clean_result { apply(rule) }
    else
      policy = policy_for(record: record, **options)
      rule = policy.resolve_rule(rule)

      policy.apply(rule)
      policy.result
    end

  if res.fail? && result&.reasons
    inline_reasons ? result.reasons.merge(res.reasons) : result.reasons.add(policy, rule, res.details)
  end

  res.clear_details

  res.success?
end
deny!(reason = nil) click to toggle source
Calls superclass method
# File lib/action_policy/policy/reasons.rb, line 222
def deny!(reason = nil)
  result&.reasons&.add(self, reason) if reason
  super()
end
details() click to toggle source

Add additional details to the failure reason

# File lib/action_policy/policy/reasons.rb, line 195
def details
  result.details ||= {}
end