Words and Code

One writer’s journey from words to code.

Using Pundit, the Cool Kid of Authorization


Ah, authorization – or as I like to call it, authentication’s cooler, slightly less complicated twin. It’s easy to confuse the two, but there’s a fundamental difference between them. I recently learned that while setting up an authorization system for an application that’s nearing the end of development. Authentication focuses on who you are as a user — an admin, guest, or user with an account, for example — while authorization is about what actions you can take. Authorization centers around what you are actually able to do within the context of your role.

It often makes sense to leave authorization as one of the later (and sometimes, last) step of development, purely because it means that you don’t need to worry about making sure that you are authenticated in your development environment while you are still building out your application. But eventually, somewhere between development and deployment, you’ll have to think about the abilities of your users — or, what they can and can’t do.

There are a few different ways to go about creating an authorization system, one of the most popular being can_can, a gem that has been around in the Rails community since 2010, as well as a newer gem called rolify. But the one that I’ve found really easy and fun to work with is pundit, an authorization system crafted by eLabs, and interestingly enough, based off of the logic and thought behind the CanCan’s approach. The developers at eLabs actually started off using the CanCan library in their own applications, but quickly realized that it could become very complicated, very quickly. So, they built the simpler authorization system of pundit, which is exactly what we’ll play around with today!

Policies of Pundit


Implementing the pundit gem is easy once we understand how it’s structured, what it’s expecting, and what conventions to follow. But before we do anything else, we’ll need to add it to our Gemfile:

gem "pundit"

and run our favorite command, bundle install.

The documentation for this gem is fairly well-explained, and even allows you to generate all the basic files you need with a single command (rails g pundit:install). However, generators can be a little bit dangerous if you don’t understand what’s going on behind the scenes, since all logic has been abstracted away and things start happening automagically! So, let’s set up our authorization system for our bookstore manually. Don’t worry — it’s not going to be too painful!

First things first: we need to make sure that we include pundit in our controllers. This is a particularly important step because we’re going to use a method inside of all of our controllers, and if we don’t include Pundit, none of our controllers will have any idea what method we’re trying to call. Since we’re going to be authorizing multiple classes, it makes sense to add pundit to the file where all our other controllers will inherit from: ApplicationController.

1
2
3
4
class ApplicationController < ActionController::Base
  include Pundit
  protect_from_forgery
end

Next, we need to understand how pundit actually works. If we take a look at the documentation, there are a few things that immediately become clear: pundit is focused around something called “policy classes”. Similar to how cancan relies “ability classes”, pundit expects a policy class to house our authorization logic.

Okay, so we need policy classes. But what kind of classes are they? Do they inherit from anything? How many do we need, exactly? And what should go inside of them? If these are the questions running through your head, fret not: there are answers to all of them!

Here are the basics of building a pundit policy class:

  1. Every policy class is just a plain old ruby class. It doesn’t need to inherit from ActiveRecord::Base, or anything else. The only thing that the class must have is the suffix Policy.
  2. Each policy class should contain the authorization logic for the model class that it corresponds to. A User model would have a UserPolicy, while a Book model would have a BookPolicy class.
  3. A policy class should initialize with two instance variables: a user, and the model that we want to authorize. The initialize method in a policy class will always expect these two parameters in a certain order: user as the first argument, and the model object that we want to authorize as the second arugment. The model doesn’t have to be an ActiveRecord object – it can literally be anything that you want to authorize!
  4. Inside of the policy class are query methods that actually contain the code for specific authorization checks. These should correspond and map to the actions in the controller for the model we want to authorize. If our UsersController has create, update, and destroy actions, our UserPolicy class should (theoretically) have create?, update?, and destroy? query methods inside of it.

Okay, that’s enough on policies to start. Let’s actually start writing some of those policy classes! We’ll need to create a folder for all of our policies since we’re not using the generator (app/policies), which will house our policy classes. The most important policy class that we should write first is our ApplicationPolicy, which will reside at app/policies/application_policy.rb. This is where we can put our initialize method in, since we know that every policy needs to have this method inside of it.

1
2
3
4
5
6
7
8
class ApplicationPolicy
  attr_reader :user, :resource

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

Since the initialize method of our ApplicationPolicy is going to be used by all of our other policy classes, we can refer to the second argument (the model that we want to authorize) as resource. Depending on what policy class we are in, the resource object will change. If we had used the generator, we’d notice that the model object is actually referred to as record, which also would have been an acceptable name for our second argument. Either way, this is a nice and easy way of abstracting out this method into one file, and then reusing it in our other policy classes. Speaking of which…it’s time for us to write those, next!

Authorize Me

We’ll start by authorizing (one of) the most important objects in our application: our users! Since we have a User model, we’ll need to abide by pundit’s policies, and create a user_policy.rb file inside of app/policies. We’ll make sure that it inherit from ApplicationPolicy, so that we’ll have access to the initialize method:

1
2
class UserPolicy < ApplicationPolicy
end

Next, let’s take a look at our UsersController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class UsersController < ApplicationController
  
  # Some more RESTful actions would go here,
  # probably like #create and #destroy 

  def update
    user = User.find(params[:id])

    user.assign_attributes(user_params)

    if user.save
      render json: user
    else
      render json: {}, status: :unprocessable_entity
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email)
    end
end

There’s nothing too fancy going on in here at the moment, and for now, we’re only concerned with authorizing the update action in our UsersController. Ultimately, we don’t want any user to be able to update their information unless they are logged in; in other words, a user shouldn’t be able to update anyone else’s name and email except for their own.

So, if we want to authorize the update action, we’ll need a query method in our UserPolicy class called — you guessed it — update?:

1
2
3
4
5
6
7
8
9
10
11
12
class UserPolicy < ApplicationPolicy

  # Inherited from ApplicationPolicy:
  # def initialize(user, resource)
      # @user = user
      # @resource = resource
  # end

  def update?
      user == resource
  end
end

All we’re doing here is verifying that the user instance that we’re passing in to the initialize method (which we’re inheriting, remember?) is the same instance as the resource that we’re passing in. The resource is the model that corresponds to the policy; in our case, we’re in the context of the UserPolicy, so our resource is the user instance.

The last step is actually telling our update action in our UsersController to use the UserPolicy and authorize our user instance. To do this, we’ll need to call the authorize method, and pass in our resource that we want to authorize:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class UsersController < ApplicationController
  after_action :verify_authorized
  
  def update
    user = User.find(params[:id])

    authorize user
    user.assign_attributes(user_params)

    if user.save
      render json: user
    else
      render json: {}, status: :unprocessable_entity
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email)
    end
end

In addition to calling authorize, we also need to make sure that our policy is actually being used! In fact, that’s what the after_action :verify_authorized line is doing. The documentation suggests adding this to our ApplicationController, but for the sake of clarify, we can put it here temporarily, just to see how everything is working together. Pundit adds a method called verify_authorized to our controllers. It’s this method that is reponsible for raising an exception if authorize is not called. It’s recommended to put this in an after_action so that we don’t forget to authorize any controller actions when we invoke them. Eventually, we’ll want to abstract this line to our ApplicationController:

after_action :verify_authorized, except: :index

Since this is an after_action, we can pass it except or only, if we want to skip authorization for certain controller actions.

So, how is all of this working? Well, when we pass user to the authorize method, we are actually telling pundit to look for a UserPolicy, find the corresponding action, and invoke it. If our user instance is not authorized, pundit will raise a Pundit::NotAuthorizedError. The source code for the policy finder and the authorize method reveal exactly how this happens, and are pretty cool to look at under the hood.

Scopin’ Out More Policies

The pundit gem is pretty powerful, but the thing that makes it the cool kid of authorization is how it handles scopes. We can use scopes when we want to have a specific subset of records that a user has access to — basically, when we want to narrow the “scope” of the resources that are visible to our user, based on their “level” of authorization (admin, guest, etc.).

Let’s write a quick scope for the Review objects of the book reviews in our bookstore. Right now, our ReviewPolicy looks like this:

1
2
3
4
5
class ReviewPolicy < ApplicationPolicy
  def update?
      resource.user == user
  end
end

We have a corresponding update action in our ReviewsController, and we’re allowing a review to be updated only by the user who wrote it (in other words, the user object that the review belongs to). But we also need an index action, and we want to limit the reviews that can be viewed by a user in the index action. Ultimately, the only user that should be able to see all reviews (including drafts) should be admins; otherwise, the only reviews that should be visible are the ones that have been published.

And that’s where scopes come into play. There’s a few rules to scopes:

  1. They are plain old ruby classes, nested within the policy class.
  2. They have an initialize method that takes a user instance, and a scope. The scope is what we’ll perform some kind of database query on — usually an ActiveRecord class or ActiveRecord::Relation.
  3. The class should have a resolve method, which should return an array of instances that we can iterate over — again, probably an ActiveRecord::Relation.

Let’s go ahead and add a scope to our preexisting ReviewPolicy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ReviewPolicy < ApplicationPolicy
  class Scope
    attr_reader :user, :scope

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

    def resolve
      if user.admin?
          scope.all
      else
              scope.where(published: true)
          end
    end
  end

  def update?
    resource.user == user
  end
end

The only thing that’s actually happening here is that we’re limiting the number and types of book reviews that will be rendered by the resolve method. If our user is an admin, we’ll run the query Review.all; otherwise, we’ll execute the query, Review.where(published: true).

Again, we could abstract the initialize method of our Scope class into the ApplicationPolicy so that we could inherit class Scope < Scope, rather that actually writing the method directly into this class. In fact, that’s probably exactly what we would do once we realized that we needed to write more than a single scope.

The last step is adding our scope to our ReviewsController. We can use a method provided by pundit in our controller called policy_scope, which takes an class instance of a model (in our case, Review):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ReviewsController < BaseController
    after_action :verify_policy_scoped, only: :index
    skip_after_action :verify_authorized, only: :index

    def index
      if user_signed_in?
        reviews = policy_scope(Review)

        render json: reviews
      else
        render json: { reviews: [] }
      end
    end

    def update
      review = Review.find(params[:id])

      authorize review

      # Logic for updating our review
    end
end

Now, our index action uses the policy_scope method to find the reviews that we’ll render. The policy_scope method infers that we want to use the ReviewPolicy::Scope class, and it will instantiate the class and then call resolve on the instance. In fact, the code that’s actually getting executed here is this:

reviews = ReviewPolicy::Scope.new(current_user, Review).resolve

The other important line in our controller is the method that’s actually mkaing sure that the policy scope is being used:

after_action :verify_policy_scoped, only: :index

Similar to verify_authorized, the verify_policy_scoped method is what ensures that the policy scope is actually being used. And in our case, we only have a scope on our index action, so we can specify that we only want to use a scope on :index. Not too bad, right? Just tell pundit what you want to scope, and what you want to authorize, and it will do the rest for us!

But what if we had a class that never needed to be authorized or scoped? How could we tell pundit to just skip the authorization for that specific model? Well, it’s pretty easy — we can just use skip_after_action:

1
2
3
4
5
6
class CommentsController < ApplicationController
  skip_after_action :verify_authorized
  skip_after_action :verify_policy_scoped

  # RESTful controller actions go here!
end

Simple! This gem is pretty fantastic to work with, and gives us great guidelines on the proper way of using it. I really liked this particular piece of advice from the library’s developers:

“Pundit strongly encourages you to model your application in such a way that the only context you need for authorization is a user object and a domain model that you want to check authorization for. If you find yourself needing more context than that, consider whether you are authorizing the right domain model, maybe another domain model (or a wrapper around multiple domain models) can provide the context you need.”

It turns out that authorizing with pundit can be a really good time. You might say that it’s even kind of…fun!

tl;dr?

  • The pundit gem is a simple way to build a powerful authorization system. It expects plain old ruby policy classes for each model that you want to authorize, and each class should contain query methods that map to controller actions for the model. The controller that corresponds to the model should call the authorize method for the object, and should either contain or inherit an after_action :verify_authorized method.
  • The developers at eLabs put a lot of work into building pundit, and based it off of the cancan library. In fact, they even wrote a super blog post about their process, which you can read over here.
  • Still confused about when to authorize and when to authenticate? Check out this awesome slidedeck that clarifies all your authorization confusion!