ruby

Authorization Gems in Ruby: Pundit and CanCanCan

Aestimo Kirina

Aestimo Kirina on

Authorization Gems in Ruby: Pundit and CanCanCan

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):

shell
bundle exec rails generate migration add_column_role_to_users role:integer

Then run the migration:

shell
bundle exec rails db:migrate

And then modify the user model to include the roles we've just defined:

ruby
# app/models/user.rb class User < ApplicationRecord # User role enum role: %i[writer editor] devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable end

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:

shell
bundle exec rails g scaffold Post title body:text user:references

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:

ruby
# app/controllers/posts_controller.rb ... def create @post = current_user.posts.new(post_params) # automatically assign a post to the current user on creation respond_to do |format| if @post.save format.html { redirect_to post_url(@post), notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end ...

We can also modify the User model to make sure it's associated with the Post model:

ruby
# app/models/user.rb class User < ApplicationRecord has_many :posts devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable enum role: %i[writer editor] end

Finally, let's seed the database with some users, each with a different role:

ruby
# db/seeds.rb User.create(email: 'writer1@example.com', password: 'example', password_confirmation: 'example', role: 0) # this creates a user with the writer role User.create(email: 'writer2@example.com', password: 'example', password_confirmation: 'example', role: 0) # second writer User.create(email: 'editor@example.com', password: 'example', password_confirmation: 'example', role: 1) # this creates a user with the editor role

Then seed the database:

shell
bundle exec rails db:seed

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:

ruby
# Gemfile gem 'pundit'

Then in the terminal, run the command:

shell
bundle install

Alternatively, run the following command:

shell
bundle add pundit

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:

ruby
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Pundit::Authorization end

And finally, generate a base policy class all other policies will inherit from:

shell
bundle exec rails g pundit:install

Which gives you the following base policy class:

ruby
# app/policies/application_policy.rb class ApplicationPolicy attr_reader :user, :record def initialize(user, record) @user = user @record = record end def index? false end def show? false end def create? false end def new? create? end def update? false end def edit? update? end def destroy? false end class Scope def initialize(user, scope) @user = user @scope = scope end def resolve raise NotImplementedError, "You must define #resolve in #{self.class}" end private attr_reader :user, :scope end end

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 called PostPolicy.
  • An attr_reader: this takes two arguments - the first is a user, 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:

shell
bundle exec rails g pundit:policy post

This gives us the following generic policy class that inherits from the base policy we generated earlier:

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy class Scope < Scope # NOTE: Be explicit about which records you allow access to! # def resolve # scope.all # end end end

Let's now add access rules for the writer role accordingly (these also override any rules that are inherited from the base policy class):

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy ... def create? @user.writer? # a writer is able to create a post end def edit? @user.writer? # a writer is able to edit a post end def update? @user.writer? # a writer can update a post end def delete? @user.writer? # a writer can delete a post end end

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:

ruby
# app/controllers/post_controller.rb ... def create @post = current_user.posts.new(post_params) authorize @post respond_to do |format| if @post.save format.html { redirect_to post_url(@post), notice: 'Post was successfully created.' } format.json { render :show, status: :created, location: @post } else format.html { render :new, status: :unprocessable_entity } format.json { render json: @post.errors, status: :unprocessable_entity } end end end ...

To test this, log in as an editor and try to create a post. Doing this will result in the following error:

Pundit's not authorized 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:

ruby
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Pundit::Authorization rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "You are not authorized to perform this action." redirect_back(fallback_location: root_path) end end

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:

Rescuing from a Pundit's not authorized error

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:

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy class Scope < Scope def resolve if user.editor? # an editor can only access posts in "draft" status scope.where(published: false) else # can access a post if they are the author scope.where(user: user) end end end def show? @user.writer? || @user.editor? end def create? @user.writer? end def edit? @user.writer? || @user.editor? end def update? @user.writer? || @user.editor? end def delete? @user.writer? end end

Then we'll authorize access to the resource in the posts controller, like so:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController ... def index @posts = policy_scope(Post) end # GET /posts/1 or /posts/1.json def show @post = policy_scope(Post).find(params[:id]) end ... end

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:

ruby
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy ... def permitted_attributes if user.editor? [:title, :body, :excerpt] else [:title, :body] end end end

Then, modify the permitted params block in the controller:

ruby
# app/controllers/posts_controller.rb class PostsController < ApplicationController ... private def post_params params.require(:post).permit(policy(@post).permitted_attributes) end end

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:

shell
bundle add cancancan

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:

shell
bundle exec rails g cancan:ability

Which generates the class object below:

ruby
# app/models/abilty.rb class Ability include CanCan::Ability def initialize(user) end end

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:

ruby
# app/models/ability.rb can actions, subjects, conditions

As an example:

ruby
# app/models/abilty.rb class Ability include CanCan::Ability def initialize(user) can :update, Post, user: user # With CanCanCan, the update action covers both the edit and update actions end end

Then, in the controller, check if an access rule exists for a particular action — using our example, the edit action:

ruby
# app/controllers/posts_controller.rb ... def edit authorize! :edit, @post end ...

With this in place, if we visit the edit post view as another writer, we get the error below:

CanCanCan access denied error

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:

ruby
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base rescue_from CanCan::AccessDenied do |exception| respond_to do |format| format.json { head :forbidden } format.html { redirect_to root_path, alert: exception.message } end end end

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:

Handling CanCanCan's access denied exception

You can even customize the error message shown to the user:

yaml
#config/locales/en.yml en: unauthorized: update: all: "You're not authorized to %{action} %{subject}." writer: "You're not authorized to %{action} other writer's posts."

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:

ruby
# app/models/ability.rb class Ability include CanCan::Ability def initialize(user) can :update, Post, user: user # only a post's author/owner can update or edit a post can :read, Post # any user can read a post or list of posts (access both show and index actions) can :destroy, Post, user: user # only a post's author/owner can delete it return unless user.editor? cannot :create, Post # an editor role cannot create a post can :update, Post # an editor can update any post end end

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:

ruby
# app/models/ability.rb class Ability include CanCan::Ability def initialize(user) anyone_abilities if user.writer? writer_abilities elsif user.editor? editor_abilities end end private def anyone_abilities can :read, Post end def writer_abilities can :update, Post, user: Current.user # we define Current.user in the application controller so that the ability class (which is a model) is able to pick up the currently logged in user end def editor_abilities cannot :create, Post # an editor role cannot create a post can :update, Post # an editor can update any post end end

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 the policy 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!

Aestimo Kirina

Aestimo Kirina

Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.

All articles by Aestimo Kirina

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps