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):
bundle exec rails generate migration add_column_role_to_users role:integer
Then run the migration:
bundle exec rails db:migrate
And then modify the user model to include the roles we've just defined:
# 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:
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:
# 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:
# 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:
# 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:
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:
# Gemfile
gem 'pundit'
Then in the terminal, run the command:
bundle install
Alternatively, run the following command:
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:
# 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:
bundle exec rails g pundit:install
Which gives you the following base policy class:
# 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 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:
bundle exec rails g pundit:policy post
This gives us the following generic policy class that inherits from the base policy we generated earlier:
# 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):
# 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:
# 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:
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:
# 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:
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:
# 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:
# 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:
# 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:
# 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:
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:
bundle exec rails g cancan:ability
Which generates the class object below:
# 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:
# app/models/ability.rb
can actions, subjects, conditions
As an example:
# 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:
# 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:
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:
# 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:
You can even customize the error message shown to the user:
#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:
# 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:
# 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 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!
Top comments (0)