Failure Reasons
When you have complex policy rules, it could be helpful to have an ability to define an exact reason for why a specific authorization was rejected.
It is especially helpful when you compose policies (i.e., use one policy within another) or want to expose permissions to client applications (see GraphQL).
Action Policy allows you to track failed allowed_to? checks in your rules.
Consider an example:
class ApplicantPolicy < ApplicationPolicy
def show?
user.has_permission?(:view_applicants) &&
allowed_to?(:show?, record.stage)
end
endWhen ApplicantPolicy#show? check fails, the exception has the result object, which in its turn contains additional information about the failure (reasons):
class ApplicationController < ActionController::Base
rescue_from ActionPolicy::Unauthorized do |ex|
p ex.result.reasons.to_h #=> { stage: [:show?] }
# or with i18n support
p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
end
endThe reason key is the corresponding policy identifier.
You can also wrap local rules into allowed_to? to populate reasons:
class ApplicantPolicy < ApplicationPolicy
def show?
allowed_to?(:view_applicants?) &&
allowed_to?(:show?, record.stage)
end
def view_applicants?
user.has_permission?(:view_applicants)
end
end
# then the reasons object could be
p ex.result.reasons.to_h #=> { applicant: [:view_applicants?] }
# or
p ex.result.reasons.to_h #=> { stage: [:show?] }Reason could also be specified for deny! calls:
class TeamPolicy < ApplicationPolicy
def show?
deny!(:no_user) if user.anonymous?
user.has_permission?(:view_teams)
end
end
p ex.result.reasons.to_h #=> { applicant: [:no_user] }In some cases it might be useful to propagate reasons from the nested policy calls instead of adding a top-level reason. You can achieve this by adding the inline_reasons: true option:
class ApplicantPolicy < ApplicationPolicy
def show?
allowed_to?(:show?, record.stage, inline_reasons: true)
end
end
class StagePolicy < ApplicationPolicy
def show?
deny!(:archived) if record.archived?
end
end
# When applying ApplicationPolicy and the stage is archived
p ex.result.reasons.details #=> { stage: [:archived] }
# Without inline_reasons we would get { stage: [:show?] } insteadSee also #186 for discussion.
Detailed Reasons
You can provide additional details to your failure reasons by using a details: { ... } option:
class ApplicantPolicy < ApplicationPolicy
def show?
allowed_to?(:show?, record.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.to_h #=> { 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>).
The additional details are especially helpful when combined with localization, 'cause you can you them as interpolation data source for your translations. For example, for the above policy:
en:
action_policy:
policy:
stage:
show?: "The %{title} stage is not accessible"And then when you call full_messages:
p ex.result.reasons.full_messages #=> The Onboarding stage is not accessibleresult.all_details
Sometimes details could be useful not only to provide more context to the user, but to handle failures differently.
In this cases, digging through the ex.result.reasons.details could be cumbersome (see this PR for discussion). We provide a helper method, result.all_details, which could be used to get all details merged into a single Hash:
class PostPolicy < ApplicationPolicy
def edit?
check?(:published?)
end
def published?
details[:not_found] = true
record.published?
end
endp ex.result.all_details #=> {not_found: true}rescue_from ActionPolicy::Unauthorized do |ex|
if ex.result.all_details[:not_found]
head :not_found
else
head :unauthorized
end
endP.S. What is the point of failure reasons?
Failure reasons helps you to write actionable error messages, i.e. to provide a user with helpful feedback.
For example, in the above scenario, when the reason is ApplicantPolicy#view_applicants?, you could show the following message:
You don't have enough permissions to view applicants.
Please, ask your manager to update your role.And when the reason is StagePolicy#show?:
You don't have access to the stage XYZ.
Please, ask your manager to grant access to this stage.Much more useful than just showing "You are not authorized to perform this action," isn't it?