Authorization Gems in Ruby: Pundit and CanCanCan
Tags: ruby
Published: 2023-03-29 10:44:50

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:

- [Pundit](https://github.com/iamaestimo/pundit_vs_cancancan_tutorial)
- [CanCanCan](https://github.com/iamaestimo/cancancan_tutorial)

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](https://github.com/heartcombo/devise) (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](https://github.com/varvet/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](https://blog.appsignal.com/images/blog/2023-03/pundit-not-authorized-error.png)

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](https://blog.appsignal.com/images/blog/2023-03/rescue-from-not-authorized-error.png)

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](https://github.com/varvet/pundit) 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](https://github.com/CanCanCommunity/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](https://blog.appsignal.com/images/blog/2023-03/cancancan-access-denied-error.png)

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](https://blog.appsignal.com/images/blog/2023-03/catching-cancancan-access-denied-error.png)

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](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/handling_access_denied.md). 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](https://github.com/CanCanCommunity/cancancan/blob/develop/docs/README.md) 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](https://blog.appsignal.com/ruby-magic)!**