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
end
When 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.details #=> { stage: [:show?] }
# or with i18n support
p ex.result.reasons.full_messages #=> ["You do not have access to the stage"]
end
end
The 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.details #=> { applicant: [:view_applicants?] }
# or
p ex.result.reasons.details #=> { 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.details #=> { 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?] } instead
See also #186 for discussion.
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.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>
).
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 accessible
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:
rescue_from ActionPolicy::Unauthorized do |ex|
if ex.result.all_details[:not_found]
head :not_found
else
head :unauthorized
end
end
P.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?