Ruby and Rails glossary

Pundit

Pundit is a Ruby gem for authorization in Rails apps. Each model gets a plain Ruby policy class whose methods, like update? and destroy?, decide whether the current user may act on a record, and a nested Scope class decides which records that user can see.

Pundit is a Ruby gem for authorization in Rails: it answers "is this user allowed to do this to this record?" with plain Ruby classes called policies. Each model gets a policy, such as PostPolicy, with one predicate method per action (show?, update?, destroy?). Your controllers call authorize @post, and Pundit finds the policy, runs the matching method, and raises an error if it returns false. A nested Scope class does the same job for lists, so each user only loads the records they may see.

Pundit handles authorization, not authentication. It assumes something else (Devise, the Rails 8 authentication generator, your own sessions) has already worked out who current_user is. The latest release is Pundit 2.5.2, published in September 2025 (checked in November 2026). Varvet maintains it, and Jonas Nicklas first released it in 2012.

Installing it

bundle add pundit
bin/rails g pundit:install

Then include the module in your base controller:

class ApplicationController < ActionController::Base
  include Pundit::Authorization
end

The generator writes app/policies/application_policy.rb. Every query method in it returns false, so a policy that forgets to define a method denies by default. new? delegates to create? and edit? delegates to update?, which keeps the form and the save behind the same rule. Restart the server after generating it so Rails picks up the new app/policies directory.

Policies

A policy is a class named after the model plus Policy. It receives the user and the record, and each method returns true or false.

# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
  def show?
    record.published? || record.author == user
  end

  def update?
    user.admin? || record.author == user
  end

  def destroy?
    user.admin?
  end
end

The record does not have to be an Active Record model. Pundit only needs an object whose class name points at a policy, and for things with no model (a dashboard, a reports page) you can pass a symbol: authorize :dashboard, :show? looks for DashboardPolicy.

Because policies are plain classes, the usual Ruby tools apply. Share rules through inheritance or a module, and test them like any other object. Pundit ships RSpec helpers in pundit/rspec:

require "pundit/rspec"

RSpec.describe PostPolicy do
  subject { described_class }

  permissions :update?, :edit? do
    it "lets the author edit their own post" do
      author = User.new
      expect(subject).to permit(author, Post.new(author: author))
    end

    it "stops other users" do
      expect(subject).not_to permit(User.new, Post.new(author: User.new))
    end
  end
end

Checking a single record with authorize

class PostsController < ApplicationController
  def update
    @post = Post.find(params[:id])
    authorize @post

    if @post.update(post_params)
      redirect_to @post
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def publish
    @post = authorize Post.find(params[:id]), :update?
    @post.publish!
    redirect_to @post
  end
end

authorize infers PostPolicy from the record and update? from the action name. When the action and the rule differ, as in publish, pass the query as the second argument. It returns the record, so you can assign and authorize in one line. Pass a class (authorize Post) when there is no instance yet, for example on new.

A denied check raises Pundit::NotAuthorizedError. Rescue it once in ApplicationController, or map it to a 403 with config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden.

class ApplicationController < ActionController::Base
  include Pundit::Authorization
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "You are not allowed to do that."
    redirect_back_or_to(root_path)
  end
end

In views, policy(@post) gives you the policy instance, so a button and the action behind it read the same rule:

<% if policy(@post).update? %>
  <%= link_to "Edit", edit_post_path(@post) %>
<% end %>

Hiding the button is a courtesy. The authorize call in the controller is what actually protects the record.

Limiting lists with policy_scope

authorize checks one record. An index page needs the opposite question: which records may this user load at all? That is the job of the policy's Scope class and its resolve method.

class PostPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true).or(scope.where(author: user))
      end
    end
  end
end
def index
  @posts = policy_scope(Post).order(created_at: :desc)
end

def show
  @post = authorize policy_scope(Post).find(params[:id])
end

resolve returns a relation, so the filter runs in SQL and you can keep chaining order, includes or pagination after it. Starting show from the scope means a record outside it returns a 404 instead of revealing that it exists. Rules that depend on who owns or belongs to a row are record-level permissions, and the Scope class is where Pundit puts them.

Why you get AuthorizationNotPerformedError

Pundit only checks what you ask it to check. Forget authorize in one action and that action is open. To catch this, Pundit gives controllers verify_authorized, which you run after every action:

class ApplicationController < ActionController::Base
  include Pundit::Authorization
  after_action :verify_pundit_authorization

  private

  def verify_pundit_authorization
    if action_name == "index"
      verify_policy_scoped
    else
      verify_authorized
    end
  end
end

If an action finishes without calling authorize, verify_authorized raises Pundit::AuthorizationNotPerformedError. verify_policy_scoped does the same for policy_scope and raises Pundit::PolicyScopingNotPerformedError, a subclass of the first. When you see either error, the fix is one of three things:

  • Add the missing authorize or policy_scope call.
  • Call skip_authorization or skip_policy_scope inside the action when skipping is deliberate.
  • Exclude public actions from the hook, for example with skip_after_action :verify_pundit_authorization, only: :show in that controller.

Because the check runs in after_action, the action has already run when it raises. The README is explicit that this is a development aid that reminds you to authorize, not a security mechanism. Treat it as a check that fails loudly in development and in your test suite, so a missing authorize never reaches a pull request unnoticed.

Pundit, CanCanCan or Action Policy

All three are maintained and work with current Rails. They differ in where the rules live.

  • Pundit puts rules in one policy class per model and keeps the API to a handful of helpers. It is the easiest to read in a code review and has no DSL to learn. You write more of the plumbing yourself.
  • CanCanCan puts every rule in a single Ability class (can :read, Post, user: user). load_and_authorize_resource loads and checks records for a whole RESTful controller, and Post.accessible_by(current_ability) turns hash conditions into a query. It is compact while the rules are simple, and the one file grows long as they get more conditional.
  • Action Policy, from Evil Martians, uses per-model policies like Pundit and calls itself "Pundit, re-visited" in its own docs. It adds what larger apps tend to bolt on: authorize! and allowed_to?, authorized_scope for lists, pre-checks shared across rules, built-in caching, and failure reasons that say why a check failed.

Pick Pundit when you want small, explicit policies you can read top to bottom. Pick CanCanCan when your rules are mostly role tables and you like them in one place. Pick Action Policy when you already know you need caching, reasons or pre-checks. Whichever you choose, the roles behind the rules usually come from role-based access control, and a policy method like user.admin? is where that model meets the code.

Pundit in an Avo app

Back-office screens usually need the same rules as the rest of the app, and duplicating them in an admin is where they drift. Teams on Avo run their Avo app through the Pundit policies they already have with the Authorization add-on: index?, show?, update? and the rest decide what each user sees in the sidebar, the buttons and the pages, and the policy's Scope filters the records on your resource Index view. Pundit is the default client, with adapters for Action Policy and CanCanCan. Avo adds policy methods for associations and file fields; the post on advanced authorization rules introduced the association ones.

Frequently asked questions

What is Pundit in Rails?

Pundit is a Ruby gem that handles authorization in Rails apps. You write one plain Ruby policy class per model, with a method for each action, and call authorize in your controllers to enforce it. A nested Scope class in the same policy limits which records a user can list.

Why does Pundit raise AuthorizationNotPerformedError?

The controller runs verify_authorized after the action, and the action finished without calling authorize. Add the authorize call, call skip_authorization if skipping is deliberate, or skip the hook for that public action. Its sibling, PolicyScopingNotPerformedError, means an action checked by verify_policy_scoped never called policy_scope.

How do Pundit policy scopes limit which records a user sees?

Each policy can define a Scope class with a resolve method that returns a filtered relation, such as all posts for admins and only published or owned posts for everyone else. Calling policy_scope(Post) in the controller runs that method, so the filter happens in the database query. Records outside the scope never load.

Should I use Pundit, CanCanCan or Action Policy?

Use Pundit for small, explicit policy classes with almost no DSL. Use CanCanCan if you prefer every rule in one Ability class and want controller loading done for you. Use Action Policy if you want Pundit-style policies plus caching, pre-checks and failure reasons built in.

Does Pundit handle authentication?

No. Pundit only decides what a known user may do. Sign-in comes from Devise, the Rails authentication generator or your own code, and Pundit reads the result through current_user (or a pundit_user method you define).

Try Avo for free