Pundit: Your New Favorite Authorization Library

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: policies 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.

Lawson Kurtz

,
Posted in Article Category: #Code
on