Pundit - Rails Authorization

Problem

As your users create and manage data, you need to decide what is visible and editable by any user in the application. You set out by scoping your domain resources to the user or to the user's team and are off to the races!

Your service grows (hooray!) and users now want separate teams inside their organization. They want certain users to have admin privileges to handle billing and team management. Users accidentally delete things and want them back so you add the ability to undo. What data is accessible to users quickly becomes unaddressable in an ad-hoc fashion. This is where Pundit comes in.

Pundit

Pundit is an authorization library for managing access to resources in an application. Pundit offers two abstractions--Policies and Scopes. Policies confirm a user has access to a specific resource. Scopes filter collections for a given user. Most commonly Pundit is found in the controller and view layer but can also be used in service objects. It effectively leverages convention over configuration, dependency injection, and inheritance to make authorization robust and maintainable.

You can understand Pundit's usage in 5 minutes of reading the readme and can understand the source code in 10 - 15 minutes more. Whether your authorization needs are simple or complex, Pundit is an excellent solution for localizing authorization logic and ensuring data authorization is considered for every endpoint in your application.

Policies

Using convention over configuration, Policies are easy to implement and understand. Each Policy's name begins with the name of the model it protects and is suffixed with Policy. The Policy implements query methods that map to controller's actions. The naming convention is then used by the PolicyFinder to infer which policy to lookup when authorize is called.

class ArticlePolicy < ApplicationPolicy  
  attr_reader :user, :article

  def initialize(user, article)
    @user    = user
    @article = article
  end

  def show?
    user.payed_for?(article) && !article.deleted?
  end
end  

The Policy api could be improved by returning the resource when authorize is successful. Doing so would avoid the need to conditionally check the success of authorize in the controller. When this hypothetical authorize api is paired with the responders gem, authorization protection is introducible without any concessions of clarity.

class Api::ArticlesController < Api::BaseController

  def show
    respond_with :api, article, serializer: ArticleSerializer
  end

  private

  def article
    @article ||= authorize Article.find(params[:id])
  end
end  

Scopes

Developers generally append scopes in the controller or add default scopes to filter collections to a specific user. Default scopes can become unwieldy and sometimes dangerous. Calling unscoped on a relation removes all scopes which is rarely the intent. Manually appending scopes in the controller can work for small applications but it is too easy to accidentally forget one.

Pundit solves this problem with Scope objects. On initialization scope objects are provided a user and a collection. The Scope implements a resolve method which filters the collection. Pretty simple.

class ArticlePolicy < ApplicationPolicy  
  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user  = user
      @scope = scope
    end

    def resolve
      if user.owner?
        scope.where(team: user.teams)
      elsif user.team_admin?
        scope.where(team: user.current_team)
      else
        scope.where(user: user)
      end
    end
  end
end  

Controllers are provided with a policy_scope helper method to simplify the call. policy_scope works similarly to authorize in looking up the necessary Scope based on naming convention and calling resolve.

class Api::ArticlesController < Api::BaseController

  def index
    respond_with :api, articles, each_serializer: ArticleSerializer
  end

  private

  def articles
    @articles ||= policy_scope Article
  end
end  

To avoid hitting the database more than once, it is common to memoize a collection. Pundit encourages using policy_scope in the views as well as in the controller providing opportunities to inadvertently hit the database multiple times. An optional cache flag for policy_scope could mitigate any performance issues while keeping the existing api usable.

def articles  
  policy_scope Article, cache: true
end  

PolicyFinder

The PolicyFinder is the workhorse of Pundit looking up Policies and Scopes behind the scenes. find relies on naming conventions to infer what Policy or Scope to load, but the convention does not have to be observed. If the object or its class implements policy_class that will be used first. Otherwise find guesses at the resource's class name.

Scopes are found by first finding the Policy and then looking for a corresponding scope in the Policy namespace. Therefore, if a controller only exposed an index action it would require a boilerplate Policy to use policy_scope. Adding a concrete ScopeFinder and an abstract Finder would make Scopes a more flexible feature for application developers.

Errors

Pundit has an excellent error story as the provided errors help prevent mis-exposure of data. Pundit offers two controller after_actions, verify_authorized and verify_policy_scoped. If authorize or policy_scope have not been called, AuthorizationNotPerformed or PolicyScopingNotPerformed are raised respectively.

If a Policy is found but no query method matches the controller action the NotAuthorizedError is raised. With the after_actions and the NotAuthorizedError, Pundit interjects itself into the development process reminding developers of the need to consider authorization logic for each new endpoint and conscientiously opt-out of Pundit's protection.

Close

Pundit is an excellent gem for basic to complex authorization rules. Policies and Scopes tell your application's authorization story clearly and succinctly allowing for easy implementation and maintenance. Its after_action verification callbacks ensure your future self or other developers do not forget to consider authorization logic for each new endpoint. These two features of ease of use and safety checks makes Pundit an absolute win for any team and their users.

What do y'all think of Pundit?

photo credit: Varanasi, sadhus via photopin (license)

Show Comments