Today, many web applications will feature pages that are publicly available — like a homepage — and more secure ones where a user has to log in to get access. The process of user registration, logging in, and tracking user session state is called "authentication".
At the same time, when dealing with logged-in users, it's necessary to separate actions and resources that are available to them depending on their user roles. For example, "admins" generally have more access than normal users. This process of separating authenticated user access is termed "authorization".
In this post, we'll explore two of the most popular authorization libraries in Ruby so far: Pundit and CanCanCan.
Let's dive in!
Setup and Prerequisites
In this article, we'll use a simple Rails 7 app featuring users and posts. Users will be assigned an "editor" or "writer" role. Such a scenario is perfect for showcasing how authorization works.
Check out the code repos for our example app with:
Even though this article focuses on authorization, the companion subject of authentication cannot be ignored.
We won't get into the details of setting up authentication as that's outside the scope of this post. You can follow the installation instructions in the Devise gem documentation (we'll pair Devise with our authorization gems).
One more thing — whether you deal with Pundit or CanCanCan, the actual work of defining your app's user roles does not happen automatically when you install either of the authorization gems. You will need to set it up manually.
Let's do that.
Defining User Roles
Let's assume you've already installed the Devise gem and set up a user model. The next step is to decide what roles the app's users will have. In our case, we'll set out the following roles:
- Writer - This user role will be able to create, edit, update, and delete their own posts. At the same time, a writer can also view other writers' posts.
- Editor - A user with the editor role can edit, update, view, and delete any user's posts, but they cannot create their own posts.
Add a column to store a user's role (using a migration to modify the user's table):
Then run the migration:
And then modify the user model to include the roles we've just defined:
While we are on this, let's go ahead and set up a Post
model with the attributes of title
and body
, as well as a reference to the user who writes the post:
We add the foreign key user_id into the Post
model to associate every post that's created with a particular user. Since we have already set up user authentication using Devise, we can simply modify the create method of the Posts
controller to automatically set the user_id to the currently logged-in user:
We can also modify the User model to make sure it's associated with the Post
model:
Finally, let's seed the database with some users, each with a different role:
Then seed the database:
So far, our app now has:
- Authentication set up using Devise.
- Two defined user roles of "writer" and "editor".
- A
Post
model.
With that, we now have everything we need to work properly with Pundit.
Authorization in Your Ruby App with Pundit
Pundit is an authorization library built around object-oriented architecture and plain Ruby classes. It gives you tools to build a solid authorization layer that can scale with your app.
Installing Pundit in Your Ruby App
Add the gem to your app's Gemfile:
Then in the terminal, run the command:
Alternatively, run the following command:
Since authorization mostly deals with granting or denying access to controller resources, the next step is to add Pundit's authorization module to the application controller:
And finally, generate a base policy class all other policies will inherit from:
Which gives you the following base policy class:
With that, Pundit is now properly set up and ready to go. Additionally, we have authentication and user roles set up.
Next, let's use policies to implement rules that define how each user role will access the Post
resource.
Configuring Pundit Policies
In Pundit lingo, a "policy" is a plain Ruby class where you define all the rules for how a user role interacts with different resources.
These policies come with some notable features:
- Each policy is named after an existing model, suffixed with the word "Policy". For example, a policy defining how the
Post
model is accessed is calledPostPolicy
. - An
attr_reader
: this takes two arguments - the first is auser
, specifically, the currently logged-in user —current_user
— and the second argument is the model that you'd like to define authorization rules for, in our case,post
. - Query methods that will map to the controller methods of the resource that has authorization rules set up. For organizational purposes, it's best to have all policies under the
app/policies
folder.
Since we know what access rules we need to define for the different user roles in our app, let's start with the writer role.
Defining a Pundit Policy for a Role
Let's use the writer role to see how this can be done. To begin with, we can outline the writer role's access to the Post
resource as follows:
- Create their own posts
- Edit and update their own posts
- View (or read) their own posts as well as other user's posts
- Delete their own posts
With that in mind, go ahead and generate a new policy to control how posts are accessed:
This gives us the following generic policy class that inherits from the base policy we generated earlier:
Let's now add access rules for the writer role accordingly (these also override any rules that are inherited from the base policy class):
Here, we define what a writer can do when creating, editing, updating, and deleting posts which are corresponding actions on the posts' controller. If you have noticed, these rules apply to posts in general and not necessarily to a writer's own posts (we'll get to that in the section on scopes).
For now, let's see how we can use this policy.
Using a Policy in Pundit
To use a policy, call Pundit's authorize
method on the controller's method where you want to check access rules. You instantiate the relevant policy class and, more specifically, the action that should be called based on where the authorize
method has been called.
For example, let's call authorize
on the post controller's create method:
To test this, log in as an editor and try to create
a post. Doing this will result in the following error:
Though this does what we want, showing such an error page is not good for the user experience. In the next section, you will learn how to rescue the NotAuthorizedError
and serve up something more user-friendly.
Rescuing From Pundit's NotAuthorizedError
First, we'll need to edit ApplicationController
as follows:
Here, we are basically telling Pundit to use the user_not_authorized
method whenever the NotAuthorizedError
is raised. It will simply redirect the unauthorized user to a specific page and also provide a relevant flash message explaining what has happened:
We now have a simple authorization system capable of handling a very generalized permissions case.
But what if we want more fine-grained permissions? For this, we need to utilize Pundit's scopes.
Pundit's Scopes
Pundit scopes are similar to ActiveRecord scopes. In the latter, you can use scopes to fetch records according to specific criteria. However, with Pundit's scopes, you manage access to specific resources according to certain rules you set.
Let's say we want editors to be able to view and edit posts that are in "draft" status, and at the same time, allow writers to create, view, edit, update and delete posts that only belong to them.
We can start by editing the post policy to look like this:
Then we'll authorize access to the resource in the posts controller, like so:
Of course, scoping with Pundit can go way deeper than we've shown, but we'll leave it at that in this post. If you want, check out the Pundit documentation to see how you can use scopes in a more advanced way.
For now, let's go through how you can use the library with Rails' strong parameters.
Using Pundit with Rails' Strong Parameters
By combining Pundit's authorization rules with Rails' strong parameters, you can achieve lockdown access to a resource's attributes. Let's say you want editors to be the only ones with access to an excerpt field of the Post
model. How would you go about it?
First, add an aptly-named block to the relevant policy:
Then, modify the permitted params block in the controller:
With that, you have made the excerpt attribute available to editors only.
We'll now shift gears to look at the other authorization library, CanCanCan.
Introducing CanCanCan for Your Ruby App
CanCanCan is an authorization library that uses an "ability" class to define who has access to what in a Rails app. Actual access control is achieved using an authorization module and various view helpers.
Installing CanCanCan
Installation is as easy as running the command below:
Just like Pundit, with CanCanCan, you can define all access rules within a plain Ruby class object called an "ability" class. Let's do that next with the following generator command:
Which generates the class object below:
Let's learn more about how the ability class can define access rules for our example Rails app next.
Defining and Checking CanCanCan Abilities
We'll use the same user roles as in the Pundit example: writers and editors. A writer can create, edit, update, destroy their own posts, and view other writers' posts; an editor can do everything except create a post of their own.
To use CanCanCan, first define what each user or role can access in the ability class, following this format:
As an example:
Then, in the controller, check if an access rule exists for a particular action — using our example, the edit
action:
With this in place, if we visit the edit post view as another writer, we get the error below:
And just like we did with Pundit, let's rescue this error and show the user a better error page.
Handling CanCanCan’s “Access Denied” Errors
Whenever a resource is not authorized, CanCanCan will raise a CanCan::AccessDenied
error. The easiest way to catch this exception is by modifying the ApplicationController
as follows:
Doing this makes for a good user experience. In the screenshot below, the unauthorized user is redirected to the home page and shown a relevant flash message:
You can even customize the error message shown to the user:
If your app serves XML as a response, or you just want to dive deeper into handling the CanCanCan::AccessDenied
exception, check out CanCanCan's documentation. For now, let's see how we can combine CanCanCan's various abilities to create a more robust authorization layer.
Combining Multiple CanCanCan Abilities
You can define multiple access rules for a resource in the ability class. Taking the writer and editor roles, for example, we can do this:
The question is, why would you want to do this?
With CanCanCan, you can define all access rules in one ability file. This has both advantages and disadvantages.
Having all your rules in one place is very convenient for handling access rules since all rules are defined in one place.
However, if your app deals with many user roles or you have several resources that need authorization, the ability class can easily become too big and complex to handle. One way to handle this is by reorganizing the ability class to use method definitions, like so:
There's a lot more to CanCanCan than can be effectively covered in this article. Do check out the detailed CanCanCan documentation to learn more.
To wrap up, let's briefly touch on the features of each library and give reasons why you might choose one over the other.
Feature Comparison: Pundit vs. CanCanCan for Your Ruby App
- File organization - With Pundit, you can easily organize your app's authorization across multiple policy files. But with CanCanCan, authorization rules will live in one ability file. Working with multiple ability files is still possible, but this is not the default implementation style.
- Tests - Because permissions code will mostly reside within a single class compared to Pundit's multiple ability classes, writing tests for CanCanCan could be easier than for Pundit.
- Helpers - Both libraries provide a number of view helpers to check for permissions in the view layer. CanCanCan gives you the
can?
method to use in your views, while Pundit gives you relatively similar functionality with thepolicy
helper. - Devise integration - As you can see from the examples we have used in the article, both libraries integrate with Devise very well.
Wrapping Up
In this article, we looked at two of the most popular authorization gems in the Ruby and Rails ecosystem: Pundit and CanCanCan.
Both libraries offer a rich set of features for managing permissions in your Rails app. Because of this, it is nearly impossible to tell you which gem to choose — both can manage even the most complex permissions setups.
We encourage you to try out Pundit and CanCanCan in your app and see which one fits your needs best.
Happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!