Pundit: Your New Favorite Authorization Library
Lawson Kurtz, Former Senior Developer
Article Category:
Posted on
Pundit is a tiny Rails authorization library that will make you jump for joy after using the likes of CanCan.
Strictly speaking, Pundit it a small, unopinionated library of helper methods designed to make it easier to interface with Policy objects (classes that define your application’s authorization logic). It’s explicit, easy to understand, easy to use, and powerful.
It’s everything an authorization library should be.
Policy Objects
Pundit’s only strict opinion is that authorization should be performed by discrete objects, called policies. Policy objects make a ton on sense (even if you aren’t using Pundit). Their goal is to completely isolate and centralize all authorization logic in your app. (Sayonara to authorization logic in your views.) A policy object and its use could be as simple as:
class Policy
def initialize(user)
@user = user
end
def authorized?
user.is_admin?
end
private
attr_reader :user
end
Policy.new(current_user).authorized?
Obviously this example is simplistic. In real applications, authorization logic can pile up and become difficult to organize. Pundit’s ease-of-use comes from its suggestion that policies be organized RESTfully as described below.
Naming + Inference
Pundit policies are named after the models for which they authorize actions. A policy for authorizing actions pertaining to the BlogPost
model would be named BlogPostPolicy
. This is for convenient inference of the appropriate policy in the contexts of a controller or view. Policies aren’t handcuffed to model names though. You can still create “headless” policies that aren’t tied to any specific model, and you can always use policies with models (less convienently) while breaking the naming convention… although at that point you wouldn’t really be using Pundit anymore.
An example of a policy directory:
Policy Actions
Pundit suggests that your policies’ public API consist of predicate methods conventionally named after the REST action they authorize. (But of course you can customize to your liking.)
#index?
#show?
#new?
#create?
#edit?
#update?
#destroy?
Within policy action methods, you have access to user
and record
. The user—current_user
if using the #authorize
helper from a controller (some more overridable inference)—is the user that is trying to perform the action. The record is the item to which the action is being applied.
Example
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
# More about this in the following examples...
scope.where(id: record.id).exists?
end
def create?
is_contributor?
end
def new?
create?
end
def update?
is_editor?
end
def edit?
update?
end
def destroy?
is_editor?
end
private
def is_editor?
user.editor?
end
def is_contributor?
is_editor? || user.author?
end
end
Use in a Controller
def edit
authorize(article)
# :point_up: is essentially the same as :point_down: just more convenient
ArticlePolicy.new(current_user, article).edit? || raise Pundit::NotAuthorizedError
end
private
def article
@article ||= Article.find(params[:id])
end
Use in a View
<%- if policy(:articles).new? -%>
<% content_for :page__nav_left do %>
<%= link_to "New Article", new_article_path %>
<% end %>
<%- end -%>
Policy Scope
Simple true
-or-false
-returning authorization methods work fine for most actions. A user can either edit a record or they can’t. But some Actions (like viewing an index of records) require more complex authorization. For these cases, Pundit uses the notion of authorized scopes to provide the granularity you need. An authorized scope is just an authorization-based limitation to the default scope of a particular model.
Authorized scopes are defined as a class with a single instance method: #resolve
. In this method, you need simply to restrict the scope to that allowed for the given user.
Example
class ArticlePolicy < ApplicationPolicy
class Scope < ApplicationScope
def resolve
# Only contributors can see unpublished Articles
is_contributor? ? scope : scope.published
end
end
def show?
# Only show the given record if it is within the authorized scope.
scope.where(id: record.id).exists?
end
# ...
private
def is_editor?
user.editor?
end
def is_contributor?
is_editor? || user.author?
end
end
Use in Controller
def index
end
private
def articles
policy_scope(Article)
# You can also chain conditions onto the return of #policy_scope
policy_scope(Article).where(author_id: current_user.id).order(published_at: :desc)
end
Ensuring Authorization
Pundit provides two methods that can ensure that authorization methods have been called for the current request:
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
Liberal use of these methods protects you from accidentally omitting authorization where it should occur.
Testing
Since policy object are just POROs, they’re easy to test. And when your authorization logic is backed by rock-solid tests, you sleep better at night.
Example
require "rails_helper"
describe ArticlePolicy do
it_behaves_like "an admin-only permissions policy"
describe described_class::Scope do
let(:user) { create(:user) }
let!(:article_0) { create(:article) }
let!(:article_1) { create(:unpublished_article) }
subject do
described_class.new(user, Article)
end
describe "#resolve" do
context "when the user is an editor" do
before do
user.update_attributes(role: :editor)
end
it "returns an unconstrained scope" do
[article_0, article_1].each do |article|
expect(subject.resolve).to include(article)
end
end
end
context "when the user is an author" do
before do
user.update_attributes(role: :author)
end
it "returns an unconstrained scope" do
[article_0, article_1].each do |article|
expect(subject.resolve).to include(article)
end
end
end
context "when the user is not a contributor" do
before do
user.update_attributes(role: :normal_person)
end
it "returns a scope including published articles" do
expect(subject.resolve).to include(article_0)
end
it "returns a scope excluding unpublished articles" do
expect(subject.resolve).to_not include(article_1)
end
end
end
end
end
Flexibility & Code Reuse
One of the greatest benefits of well-implemented use of an authorization library like Pundit is the ability to reuse code, especially in multi-user role context. In many multi-role apps, controllers become a duplicative mess of variants that differ only by the inclusion of a particular action, or the restriction of a certain scope. When you offload all authorization logic to policies, you’re able to write a single, generic version of a controller, and reuse it for every role without fear of exposing unauthorized information or abilities.
module Editor
class ArticlesController < Editor::Controller
include ArticleManagement # This includes generic actions for reuse with all roles.
end
end
And with this reuse (and if you’re using Pundit view helpers to appropriately hide UI based on authorization), when authorization logic eventually needs to change, a simple change to the policy is all that’s required. It’s difficult to express how awesome it feels to make a one-line update that fundamentally changes how a user can interact with the app.
In Closing
Authorization is an important aspect of nearly every modern web application. And when it comes to the security of your app, it’s critical that you understand exactly what is going on behind the scenes. Pundit is so lightweight, developing a full understanding of its inner workings is a trivial affair. And despite its small size, Pundit still provides powerful, easy-to-use tools that can dramatically improve your application.
So next time you reach for an authorization library, give Pundit a shot.