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)
end
end

Call 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 post

The 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)
end
end

Implicit 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)
end
end

Checking 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
end
end

Practical 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 :user
end

API 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
end
end

Interactors / 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
end
end

What’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.

Powered by WebContainers