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 |
|
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:
- 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 suffixPolicy
. - Each policy class should contain the authorization logic for the model class that it corresponds to. A
User
model would have aUserPolicy
, while aBook
model would have aBookPolicy
class. - A policy class should initialize with two instance variables: a
user
, and themodel
that we want to authorize. Theinitialize
method in a policy class will always expect these two parameters in a certain order:user
as the first argument, and themodel
object that we want to authorize as the second arugment. Themodel
doesn’t have to be anActiveRecord
object – it can literally be anything that you want to authorize! - 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
hascreate
,update
, anddestroy
actions, ourUserPolicy
class should (theoretically) havecreate?
,update?
, anddestroy?
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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:
- They are plain old ruby classes, nested within the policy class.
- They have an initialize method that takes a
user
instance, and ascope
. Thescope
is what we’ll perform some kind of database query on — usually an ActiveRecord class orActiveRecord::Relation
. - The class should have a
resolve
method, which should return an array of instances that we can iterate over — again, probably anActiveRecord::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 |
|
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 |
|
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 |
|
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 theauthorize
method for the object, and should either contain or inherit anafter_action :verify_authorized
method. - The developers at eLabs put a lot of work into building
pundit
, and based it off of thecancan
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!