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.
ruby
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
endAdvanced usage
Where the library earns its place over a simpler alternative.
ruby
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
endErrors 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.
