Non-Rails Usage
Action Policy is not limited to Rails controllers. Because authorization logic lives in plain Ruby policy classes, the same policies work anywhere Ruby runs — service objects, background jobs, CLI tools, API layers, and more.
The Core Module: ActionPolicy::Behaviour
The ActionPolicy::Behaviour module is what Rails controllers include under the hood. You can include it in any Ruby class to get the full authorization API:
class PostUpdateAction include ActionPolicy::Behaviour
# Declare the authorization subject — works like current_user in controllers authorize :user
attr_reader :user
def initialize(user) @user = user end
def call(post, params) authorize! post, to: :update? post.update!(params) endendCall it like any service object:
action = PostUpdateAction.new(current_user)action.call(post, { title: "New title" })# Raises ActionPolicy::Unauthorized if the user cannot update the postThe authorize :user declaration tells Action Policy where to find the authorization subject. When authorize! is called, it looks up PostPolicy (or whatever policy applies to the record), instantiates it with user: from the context, and runs the rule.
Explicit Authorization Context
For classes that receive the user as a method argument rather than at initialization, you can pass context explicitly:
class ReportExporter include ActionPolicy::Behaviour
def export(report, user:) authorize! report, to: :export?, context: { user: user } generate_export(report) endendImplicit Authorization Target
When your class always authorizes against the same object, use implicit_authorization_target to avoid repeating it on every call:
class DocumentService include ActionPolicy::Behaviour authorize :user
attr_reader :user, :document
def initialize(user, document) @user = user @document = document end
# Any call to authorize! without an explicit target uses this def implicit_authorization_target document end
def publish! authorize! to: :publish? # authorizes against document document.update!(published: true) end
def archive! authorize! to: :archive? # also authorizes against document document.update!(archived: true) endendChecking Permissions Without Raising
Use allowed_to? when you want a boolean check rather than an exception:
class NotificationService include ActionPolicy::Behaviour authorize :user
attr_reader :user
def initialize(user) @user = user end
def notify_if_allowed(target, message) if allowed_to?(:receive_notifications?, target) send_notification(target, message) else Rails.logger.info "Notification skipped: user #{user.id} not authorized" end endendPractical Use Cases
Background Jobs
Use ActionPolicy::Behaviour in jobs that perform actions on behalf of users. Be careful: the user who enqueued the job may have different permissions by the time the job runs.
class PublishProductJob < ApplicationJob include ActionPolicy::Behaviour authorize :user
def perform(product_id, user_id) @user = User.find(user_id) product = Product.find(product_id)
# Check that the user still has permission at execution time authorize! product, to: :update? product.update!(published: true) rescue ActionPolicy::Unauthorized => e Rails.logger.warn "Job skipped: #{e.message}" end
private
attr_reader :userendAPI Layer / Form Objects
Form objects and command objects are excellent places for authorization:
class CreateOrderForm include ActionPolicy::Behaviour authorize :user
attr_reader :user, :params
def initialize(user, params) @user = user @params = params end
def submit authorize! Order, to: :create?
order = Order.new(params) order.user = user
if order.save { success: true, order: order } else { success: false, errors: order.errors } end endendInteractors / Command Pattern
class TransferFundsCommand include ActionPolicy::Behaviour authorize :user
attr_reader :user
def initialize(user) @user = user end
def call(from_account, to_account, amount) authorize! from_account, to: :transfer?
ActiveRecord::Base.transaction do from_account.debit!(amount) to_account.credit!(amount) end endendWhat’s Not Available
A few Rails-specific features don’t apply outside of controllers and views:
verify_authorized(the after-action callback) — this is a Rails concern; implement your own enforcement strategy- Automatic rule inference from controller action name — you must always specify
to: :rule?explicitly
Everything else — policy lookup, memoization, caching, failure reasons, I18n — works the same way outside Rails.