Authorization gems for Rails: Pundit, CanCanCan and Action Policy

Authentication tells you who the user is. Authorization decides whether that user may edit this invoice, see that project or delete an account. Rails has no built-in authorization layer, so this is still a job for a gem, or for your own code.
Versions quoted are from RubyGems.org, October 2026.
Three ways to describe permissions
The main libraries differ less in what they can express than in where the rules live:
- Per-resource policy classes (Pundit, Action Policy): one Ruby class per model, with a method per action.
- A central ability file (CanCanCan): all rules for a user declared in one place.
- Hand-written checks: methods on your models or a small concern, with no gem at all.
Pundit
Pundit (2.5.2, September 2025) is deliberately small. A policy is a plain Ruby object initialised with the user and the record:
class InvoicePolicy < ApplicationPolicy
def update?
user.admin? || record.owner == user
end
class Scope < ApplicationPolicy::Scope
def resolve
user.admin? ? scope.all : scope.where(owner: user)
end
end
end
In the controller you call authorize @invoice, which infers the policy and the action, and policy_scope(Invoice) for index pages. An after_action :verify_authorized hook raises an error if a controller action forgot to authorise, which is one of Pundit's most valuable safety nets. Policies are trivial to unit-test because they are just Ruby objects.
Strengths: very little magic, easy to read, easy to test. Limits: no built-in caching, no structured explanation of why access was denied, and some repetition across similar policies.
CanCanCan
CanCanCan (3.7.0, October 2026) is the community continuation of CanCan, the authorization plugin many developers met through RailsCasts in the Rails 3 era. Rules are declared in an Ability class:
class Ability
include CanCan::Ability
def initialize(user)
can :read, Article, published: true
return unless user
can :manage, Article, author_id: user.id
can :manage, :all if user.admin?
end
end
Hash conditions can be turned into SQL, so Article.accessible_by(current_ability) returns only the records a user may read, and load_and_authorize_resource loads and checks records in RESTful controllers. Views use can? :update, @article.
Strengths: concise for role-based applications, and seeing all permissions in one file is useful for audits. Limits: the ability file grows large in complex domains, and rules defined with blocks cannot be converted to SQL scopes, which surprises people when accessible_by raises an error.
Action Policy
Action Policy (0.7.7, September 2026) uses policy classes like Pundit, but adds features that larger applications end up building themselves: rule aliases and default rules, pre-checks (for example, "admins may do anything"), caching of authorization results, scoping for several targets (Active Record relations, arrays, GraphQL), failure reasons with i18n messages, and instrumentation. It also provides test helpers and integrations for GraphQL-Ruby.
Strengths: structure and performance for complex permission systems; good fit for APIs. Limits: more concepts to learn; a smaller community than Pundit or CanCanCan, though the project is actively released.
Roles are a separate question
None of these libraries decides how roles are stored. A boolean admin column, an enum, a join table or a gem such as Rolify are all compatible with any of them. Keep the role model as simple as your product allows: many applications that think they need arbitrary roles actually need two or three fixed ones plus ownership checks.
When no gem is enough
For small applications with a single rule ("users can only see their own records"), a scoped query such as current_user.invoices.find(params[:id]) is the most robust form of authorization there is: a record the user does not own simply cannot be found. Add a library when the rules start to differ by action, role or record state, or when you want a guarantee, like Pundit's verify_authorized, that no controller was forgotten.
Testing authorization
Whatever you choose, test permissions at two levels. Unit tests on the policy or ability cover the matrix of users and actions cheaply; Pundit's and Action Policy's plain classes make this especially easy, and CanCanCan provides RSpec matchers such as be_able_to. Then keep a few request specs that prove the controller actually enforces the rule, since a perfect policy is useless if an action never calls it. Our testing guide covers the surrounding tools.
Old plugins you may still find
Applications that started before 2012 sometimes still carry declarative_authorization, early CanCan (the original cancan gem, whose last release was in 2013), or home-grown role plugins. CanCan code usually migrates to CanCanCan with few changes, since the DSL is the same. declarative_authorization has had no release since 2013, and its rules are generally easiest to rewrite as Pundit or Action Policy classes.
Summary
| Library | Rules live in | Scopes | Best for |
|---|---|---|---|
| Pundit | One policy class per model | Scope#resolve | Most applications; simplicity |
| CanCanCan | One Ability class | accessible_by from hash rules | Role-based apps, admin panels |
| Action Policy | Policy classes with aliases and pre-checks | Multiple scope types | Large apps, APIs, GraphQL |
Admin frameworks have their own preferences: ActiveAdmin has adapters for both Pundit and CanCanCan, for instance. See Admin panels if that is your next decision.
Category guidesAuthentication
Comparing Rails authentication options in 2026: the Rails 8 authentication generator, Devise, Rodauth, Authlogic, Clearance and Sorcery, plus OmniAuth for social login.
Category guidesAdmin panels
How ActiveAdmin, Avo, Administrate, RailsAdmin and Madmin approach back-office interfaces for Rails, their dependencies and licences, and when to build your own admin.
Plugin historiesAuthlogic
What Authlogic is, how its UserSession model works, its status in 2026 (still maintained, supporting Rails 7.2 to 8.1), and how it compares with Devise, Rodauth and the Rails 8 generator.