Tracking Failures
When authorization fails, a generic “not authorized” message isn’t very helpful. Action Policy tracks failure reasons so you can provide specific, actionable feedback.
The Problem with Generic Messages
rescue_from ActionPolicy::Unauthorized do |ex| redirect_to root_path, alert: "You are not authorized."endThis tells users nothing about why they were denied. Was it because:
- They’re not logged in?
- They don’t own the resource?
- The resource is archived?
Accessing Failure Reasons
When ActionPolicy::Unauthorized is raised, it includes a result object with reasons:
rescue_from ActionPolicy::Unauthorized do |ex| # The policy that denied access ex.policy #=> ProductPolicy
# The rule that failed ex.rule #=> :update?
# The result object ex.result #=> ActionPolicy::Result
# Failure reasons ex.result.reasons.to_h #=> { product: [:update?] }endUpdate the Error Handler
Open and update the error handler:
class ApplicationController < ActionController::Base include Authentication
authorize :user, through: -> { Current.user } verify_authorized
rescue_from ActionPolicy::Unauthorized do |ex| message = case ex.rule when :create?, :new? "You must be logged in to create products." when :update?, :edit? "You can only edit your own products." when :destroy? "Only administrators can delete products." else "You are not authorized to perform this action." end
redirect_to products_path, alert: message endendNow users get specific feedback!
Using allow! and deny! with Reasons
You can provide reasons when denying access:
class ProductPolicy < ApplicationPolicy def update? deny!(:not_owner) unless owner? deny!(:archived) if record.archived? true end
private
def owner? user.id == record.user_id endendAccess the reason:
ex.result.reasons.to_h #=> { product: [:not_owner] }# orex.result.reasons.to_h #=> { product: [:archived] }Nested Policy Reasons
When policies call other policies, reasons are tracked through the chain:
class CommentPolicy < ApplicationPolicy def update? # Check if user can update the parent post allowed_to?(:update?, record.post) endendIf the post policy denies access:
ex.result.reasons.to_h #=> { post: [:update?] }The full_messages Helper
For human-readable messages, use full_messages with I18n:
en: action_policy: policy: product: update?: "You cannot edit this product" destroy?: "You cannot delete this product"
# In controllerex.result.reasons.full_messages #=> ["You cannot edit this product"]Test Failure Reasons
Let’s see failure reasons in action:
- Log out (or use an incognito window)
- Try to create a new product
- You should see: “You must be logged in to create products.”
$ bin/rails consolestore(dev)> policy = ProductPolicy.new(Product.first, user: nil)=> #<ProductPolicy:0x...>store(dev)> result = policy.apply(:create?)=> falsestore(dev)> policy.result.reasons.to_h=> {} # No nested reasons in this simple caseNext, let’s learn how to add detailed context to failure reasons!
Files
Preparing Environment
- Preparing Ruby runtime
- Prepare development database
- Starting Rails server