Detailed Reasons
Sometimes you need more than just the rule name - you need specific details about why authorization failed.
Adding Details to Reasons
Use the details hash to provide additional context:
class ProductPolicy < ApplicationPolicy def update? deny!(:not_owner) unless owner?
if record.archived? details[:archived_at] = record.archived_at deny!(:archived) end
true endendAccess the details:
ex.result.reasons.details#=> { product: [{ update?: { archived_at: "2024-01-01" } }] }Real-World Example
Let’s update our policy with detailed failure tracking:
# frozen_string_literal: true
class ProductPolicy < ApplicationPolicy relation_scope do |relation| if user&.admin? relation else relation.where(published: true) end end
def index? true end
def show? true end
def create? unless user.present? details[:reason] = :not_logged_in return deny! end true end
alias_rule :new?, to: :create?
def update? unless user.present? details[:reason] = :not_logged_in return deny! end true end
alias_rule :edit?, :destroy?, to: :update?endUsing all_details
The all_details method merges all details into a single hash:
rescue_from ActionPolicy::Unauthorized do |ex| details = ex.result.all_details
if details[:reason] == :not_logged_in redirect_to new_session_path, alert: "Please log in first." elsif details[:reason] == :not_owner redirect_to products_path, alert: "You don't own this product." else redirect_to root_path, alert: "Access denied." endendI18n Integration
Combine details with I18n for localized messages:
Create :
en: action_policy: policy: product: create?: "You must log in to create products" update?: "You cannot edit this product" destroy?: "Only admins can delete products"Use full_messages:
rescue_from ActionPolicy::Unauthorized do |ex| messages = ex.result.reasons.full_messages
if messages.any? redirect_to products_path, alert: messages.join(". ") else redirect_to products_path, alert: "Access denied." endendDetails with Interpolation
You can use details for I18n interpolation:
# Policydef update? details[:product_name] = record.name deny!(:not_owner) unless owner? trueend
# Locale fileen: action_policy: policy: product: not_owner: "You cannot edit '%{product_name}'"
# Resultex.result.reasons.full_messages#=> ["You cannot edit 'T-Shirt'"]Practical Pattern: Conditional Responses
Use details to drive different responses:
rescue_from ActionPolicy::Unauthorized do |ex| details = ex.result.all_details
case details[:reason] when :not_logged_in store_location_and_redirect_to_login when :subscription_required redirect_to pricing_path, alert: "Upgrade your plan to access this feature" when :rate_limited render json: { error: "Too many requests" }, status: :too_many_requests else head :forbidden endendTest Detailed Reasons
$ bin/rails consolestore(dev)> policy = ProductPolicy.new(Product.first, user: nil)=> #<ProductPolicy:0x...>store(dev)> policy.apply(:create?)=> falsestore(dev)> policy.result.all_details=> {:reason=>:not_logged_in}Now let’s look at how to localize these authorization messages using I18n.
Files
Preparing Environment
- Preparing Ruby runtime
- Prepare development database
- Starting Rails server