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



189
190
191
# File 'lib/action_policy/policy/reasons.rb', line 189

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

Instance Method Details

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

Returns:

  • (Boolean)


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# 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) ⇒ Object



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

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

#detailsObject

Add additional details to the failure reason



195
196
197
# File 'lib/action_policy/policy/reasons.rb', line 195

def details
  result.details ||= {}
end