Module: ActionPolicy::Policy::Reasons

Included in:
Base
Defined in:
lib/action_policy/policy/reasons.rb

Overview

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>).

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



191
192
193
# File 'lib/action_policy/policy/reasons.rb', line 191

def included(base)
  base.result_class.prepend(ResultFailureReasons)
end

Instance Method Details

#allowed_to?(rule, record = :__undef__, inline_reasons: false, **options) ⇒ Boolean

Returns:

  • (Boolean)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/action_policy/policy/reasons.rb', line 201

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
      apply_r(rule)
    else
      policy = policy_for(record: record, **options)
      rule = policy.resolve_rule(rule)

      policy.apply_r(rule)
    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) ⇒ Object



223
224
225
226
# File 'lib/action_policy/policy/reasons.rb', line 223

def deny!(reason = nil)
  result&.reasons&.add(self, reason, result.details) if reason
  super()
end

#detailsObject

Add additional details to the failure reason



197
198
199
# File 'lib/action_policy/policy/reasons.rb', line 197

def details
  result.details ||= {}
end