Namespaced Policies
So far, all our policies live flat in . But real applications often have multiple contexts — a public storefront, an admin dashboard, an API — each with different authorization rules for the same resources.
Namespaced policies let you define separate policy classes per context, all scoped to a Ruby module.
The Scenario
We’re adding an admin dashboard at /admin/products. Admins need a different view: they can see everything, including drafts, and the product list has management actions.
The challenge: Admin::ProductsController authorizes the same Product model, but needs stricter rules (admin-only) and a different scope (all products, not just published).
How Namespace Lookup Works
When authorize! runs inside Admin::ProductsController, Action Policy doesn’t just look for ProductPolicy. It first checks for a namespaced policy matching the controller’s module:
Admin::ProductsController authorizing Product → looks for Admin::ProductPolicy → if not found, falls back to ProductPolicyThis cascade is automatic — no configuration needed. The namespace is inferred from the controller’s module (Admin), and Action Policy checks Admin::ProductPolicy before the top-level ProductPolicy.
Setting Up the Admin Namespace
The routes are already set up with an admin namespace. Open to see it:
namespace :admin do resources :products, only: [:index]endThis generates the /admin/products path and routes to Admin::ProductsController#index.
The Admin Controller
Open . The controller is already scaffolded:
module Admin class ProductsController < ApplicationController def index @products = Product.all authorize! end endendRight now it loads all products and calls authorize!. The authorize! call with no arguments uses the current action name (index) and infers the policy from the controller — which means it will look for Admin::ProductPolicy first.
The Starter Admin Policy
Open . It’s currently a skeleton:
module Admin class ProductPolicy < ApplicationPolicy # Admin area: only admins can access endendIt inherits from ApplicationPolicy, which already has the allow_admins pre-check. But we need to add an explicit index? rule and a scope.
Adding the Admin Policy Rules
Update :
module Admin class ProductPolicy < ApplicationPolicy def index? user.admin? end
relation_scope do |relation| relation.all # Admins see everything in admin area end endendThe index? rule is explicit: only admins. The relation_scope returns all products — no filtering by published status, because admin needs to manage drafts too.
Using authorized_scope in the Admin Controller
Update to use authorized_scope:
module Admin class ProductsController < ApplicationController def index @products = authorized_scope(Product.all) authorize! end endendNow the controller gets its product list through the policy scope. Since Action Policy finds Admin::ProductPolicy for this controller, it uses the relation_scope defined there — returning all products regardless of published status.
Demo: Admin vs Regular User
Start the server and visit /admin/products.
- As admin (
admin@example.com): you see all products including drafts, and the page loads successfully. - As a regular user (
user@example.com): you’re redirected with an “unauthorized” flash becauseindex?returns false for non-admins. - Logged out: same redirect.
The Fallback in Action
Here’s something interesting: if you delete (or rename) , Action Policy falls back to ProductPolicy. Try commenting out the file contents to see what happens:
Without Admin::ProductPolicy, the admin controller would use the public ProductPolicy. Regular users could access /admin/products (because ProductPolicy#index? returns true), and they’d only see published products (because ProductPolicy’s scope filters drafts).
This fallback is useful during development — you can add namespaced policies incrementally. But it can also mask authorization bugs if you’re not careful.
Disabling the Fallback with strict_namespace
To prevent the cascade and require an explicit namespaced policy, use strict_namespace:
module Admin class ProductsController < ApplicationController # Require Admin::ProductPolicy to exist — no fallback self.authorization_strict_namespace = true
def index @products = authorized_scope(Product.all) authorize! end endendWith strict_namespace enabled, if Admin::ProductPolicy doesn’t exist, Action Policy raises a ActionPolicy::NotFound error rather than falling back silently.
Custom Namespace with authorization_namespace
Sometimes the namespace you want doesn’t match the controller’s module name. You can override it by defining authorization_namespace in the controller:
module Admin class ProductsController < ApplicationController # Use "SuperAdmin" namespace instead of "Admin" def authorization_namespace "SuperAdmin" end
def index @products = authorized_scope(Product.all) authorize! end endendThis tells Action Policy to look for SuperAdmin::ProductPolicy instead of Admin::ProductPolicy. Useful when you have a deeper hierarchy or want to share policies across multiple admin modules.
You can also return nil from authorization_namespace to skip namespace lookup entirely and use the top-level policy:
def authorization_namespace nil # Always use ProductPolicy, skip Admin:: lookupendOrganizing Your Policy Namespace
As your application grows, you might have multiple namespaced policies. A common layout:
app/policies/ application_policy.rb ← Base class product_policy.rb ← Public storefront rules admin/ application_policy.rb ← Admin base class (optional) product_policy.rb ← Admin area rules api/ v1/ product_policy.rb ← API v1 rulesIf you create a namespace-specific base class (Admin::ApplicationPolicy), you can add shared behavior like verifying user.admin? once:
module Admin class ApplicationPolicy < ::ApplicationPolicy pre_check :require_admin
private
def require_admin deny! unless user&.admin? end endendThen all admin policies inherit from Admin::ApplicationPolicy instead of ::ApplicationPolicy.
Testing Namespaced Policies
You can test Admin::ProductPolicy just like any other policy:
$ bin/rails consolestore(dev)> admin = User.find_by(email_address: "admin@example.com")store(dev)> policy = Admin::ProductPolicy.new(nil, user: admin)store(dev)> policy.index?=> true
store(dev)> regular = User.find_by(email_address: "user@example.com")store(dev)> policy = Admin::ProductPolicy.new(nil, user: regular)store(dev)> policy.index?=> false
store(dev)> # Test the scopestore(dev)> policy = Admin::ProductPolicy.new(nil, user: admin)store(dev)> policy.apply_scope(Product.all, type: :relation).count=> 5 # All products, including draftsNotice that when testing the admin policy directly, you instantiate Admin::ProductPolicy explicitly — the namespace lookup only happens automatically inside controllers.
What We Built
Admin::ProductPolicy— admin-only authorization with an unrestricted scopeAdmin::ProductsController— usesauthorized_scopeto get the admin-filtered list- The namespace cascade in action: automatic lookup, fallback, and strict mode
authorization_namespacefor custom namespace logic
Next, we’ll look at failure reasons — understanding precisely why authorization failed and surfacing that information to users.
- Preparing Ruby runtime
- Prepare development database
- Starting Rails server