# 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

```sh
bundle add pundit
bin/rails g pundit:install
```

Then include the module in your base controller:

```ruby
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.

```ruby
# 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`:

```ruby
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`

```ruby
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`.

```ruby
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:

```erb
<% 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.

```ruby
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
```

```ruby
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](https://avohq.io/glossary/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:

```ruby
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](https://avohq.io/glossary/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](https://avohq.io/addons/authorization): `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](https://avohq.io/blog/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).
