Skip to content

What it is

Pundit provides object-oriented authorisation for Rails using plain Ruby policy classes, one per model.

A policy class per model defines predicate methods matching controller actions. Scopes filter collections to what the user may see.

Installation

gem 'pundit'

Getting started

The smallest useful thing you can do with it, and what each part means.

Policies and scopes
class BookPolicy < ApplicationPolicy
  def show?    = record.published? || owner?
  def update?  = owner? || user.admin?
  def destroy? = user.admin?

  # Filters index queries to permitted records.
  class Scope < ApplicationPolicy::Scope
    def resolve
      return scope.all if user.admin?
      scope.where(published: true).or(scope.where(author_id: user.id))
    end
  end

  private

  def owner? = record.author_id == user.id
end

class BooksController < ApplicationController
  include Pundit::Authorization
  after_action :verify_authorized, except: :index
  after_action :verify_policy_scoped, only: :index

  def show
    @book = authorize Book.find(params[:id])   # raises if not permitted
  end

  def index
    @books = policy_scope(Book)
  end
end
verify_authorized and verify_policy_scoped are the important safeguards: they fail the request if a developer forgets the authorisation call entirely, which is the failure mode that actually causes breaches.

Advanced usage

Where the library earns its place over a simpler alternative.

Handling denials and testing policies
class ApplicationController < ActionController::Base
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    # Do not reveal whether the record exists.
    render json: { error: 'not found' }, status: :not_found
  end
end

# Policies are plain classes, so they test directly.
RSpec.describe BookPolicy do
  subject { described_class.new(user, book) }

  context 'for the author' do
    let(:user) { build_stubbed(:user) }
    let(:book) { build_stubbed(:book, author_id: user.id) }

    it { is_expected.to permit_action(:update) }
  end
end
Returning 404 rather than 403 for an unauthorised record avoids confirming that it exists — a small detail that prevents enumeration of private resources.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

Pundit::AuthorizationNotPerformedError
The action never called authorize. That is the guard working — add the call or skip it explicitly.
Users see records they should not in index
policy_scope was not used. Authorising each record does not filter a collection.

Best practices

  • Enable verify_authorized and verify_policy_scoped so a forgotten check fails loudly.
  • Always use policy_scope for index actions; authorising individual records does not filter lists.
  • Return 404 rather than 403 where existence itself is sensitive.
  • Test policies directly — they are plain classes and the tests are fast.

Background

Why it exists, and what it was reacting to.

Pundit keeps authorisation as ordinary Ruby rather than a DSL: a policy is a class with predicate methods, which makes rules easy to read, test and reason about.