Ryan Bates originally released CanCan in 2009. The gem was written to handle Authorization, the problem of deciding which resources a given user is allowed to access. In 2013 all serious development on CanCan stopped when Ryan took a break from Rails development. In an effort to keep the gem going, the community forked it and created CanCanCan.
CanCanCan is still under active development today but it follows much of the same principles and design as Ryan’s original CanCan project. The question is, should you use CanCanCan today, in 2017?
CanCanCan assumes that you have
current_user defined in your controllers so you’ll probably want to use something like Devise for authentication.
Adding authorization rules with CanCanCan is a two step process: defining the ability and then checking for the ability.
The gem creates an ability file for you to define permissions. Here is a simple ability file:
class Ability include CanCan::Ability def initialize(user) if user.admin? can :manage, :all else can :read, :all end end end
can method is used to define permissions. The method accepts two arguments: the action that the user can perform and the resource that the user can perform it on:
can :manage, Article # user can perform any action on the article can :read, :all # user can read any object can :manage, :all # user can perform any action on any object
CanCanCan provides the
cannot? methods for checking the
current_users permissions in views and controllers:
<% if can? :update, @article %> <%= link_to "Edit", edit_article_path(@article) %> <% end %> <% if cannot? :update, @article %> <h1>You don't have access</h1> <% end %>
Additionally, CanCanCan provides the
authorize! method to be used in controllers. The method will raise an exception if the user is not able to perform the given action:
def show @article = Article.find(params[:id]) authorize! :read, @article end
authorize! method is successful then it will load the resource(s) into an instance variable. You can also use
load_and_authorize_resource as a shorthand so that you don’t have to use
authorize! in every controller action.
Benefits of CanCanCan
The benefits of this gem are, I believe, for small projects. CanCanCan is very easy to setup and the ability file allows developers to see all of the abilities in one place.
CanCan (and by extension CanCanCan) is also one of the oldest authorization gems so you’ll find that it is used on many Rails projects. For that reason I think that it’s definitely worth knowing, even if it’s not your favourite gem from this list.
Drawbacks of CanCanCan
Having all abilities in one place is great for small projects but for large projects this file can become large and unwieldy. I’ve worked on projects that have 1000+ line ability files with many conditional statements. It becomes very difficult to make changes to the file and it’s easy to break things that aren’t covered by tests.
I also consider CanCanCan’s scopes to be a drawback. Because the entire ability file is evaluated every time the application makes a call to check the users ability, a slow query in the ability file can bring the whole site down.
This gem allows you to set up authorization using regular Ruby classes and object oriented design patterns.
The basis of Pundit is the notion of policy classes:
class PostPolicy attr_reader :user, :post def initialize(user, post) @user = user @post = post end def update? user.admin? or not post.published? end end
A policy class is initialized with the
current_user and a model object (whose authorization you want to check). The policy class implements some query methods which usually map to controller actions. These query methods decide whether the user has permission to perform the given action, for the resource.
You can check whether a user is authorized, in a controller, using the
def update @post = Post.find(params[:id]) authorize @post if @post.update(post_params) redirect_to @post else render :edit end end
authorize @post will raise an exception unless the current user is allowed to perform the action.
You can also check a user’s permissions in a view template:
<% if policy(@post).update? %> <%= link_to "Edit post", edit_post_path(@post) %> <% end %>
If you want to restrict which records a user has access to, you can use scopes. With Pundit, scopes work within a Pundit policy class:
class PostPolicy < ApplicationPolicy class Scope < Scope def resolve if user.admin? scope.all else scope.where(published: true) end end end def update? user.admin? or not post.published? end end
Now, rather than getting all posts in your controller you can just get the ones that the current user has permission to see:
def index @posts = policy_scope(Post) end
Benefits of Pundit
The benefits of Pundit, over something like CanCanCan, is that Pundit is much more Object Oriented. This gives you a number of benefits. With CanCanCan you have a large
ability.rb file to maintain. With Pundit you have small
Policy classes that sit alongside your
ActiveRecord classes. This also has a big benefit on performance because when checking abilities Pundit only needs to check a single class whereas CanCanCan has to run through the entire ability file. Lastly, I think scopes are a lot easier to use in Pundit than CanCanCan because you can write a plain ruby method whereas CanCanCan requires you to work with Ruby blocks.
Drawbacks of Pundit
The drawbacks of Pundit are also a result of its object oriented nature. Having multiple policy classes makes it more difficult for beginner Ruby developers to understand. Multiple policy classes also mean that there is no central place to see all of your permission rules. However, I would argue that the benefit of being able to see all of your rules in one place quickly diminishes as your app grows.
This gem is ORM-neutral, has a simple syntax and allows you to group models under one plain ruby Authorizer class.
First, you need to tell Authority which model represents the users in your system:
class User include Authority::UserAbilities ... end
Then you need to tell your models which authorizer class to use:
class Article # Adds `creatable_by?(user)`, etc include Authority::Abilities # Without this, 'ArticleAuthorizer' is assumed; # if that doesn't exist, 'ApplicationAuthorizer' self.authorizer_name = 'AdminAuthorizer' ... end
The authorizer class is how you configure authorization logic in Authority. You can write methods like
readable_by? to configure who can read the resource. An authorizer looks something like this:
# app/authorizers/schedule_authorizer.rb class ScheduleAuthorizer < ApplicationAuthorizer def self.readable_by?(user) true end def self.creatable_by?(user) user.manager? end end
In this case, anyone can read a
Schedule and any user, who is a manger, can create a
Schedule record. Authorizer methods don’t map to controller actions like they do in Pundit. Instead they map to CRUD actions.
Once you’ve configured your authorizer class, you can check abilities in your controller:
class ArticlesController < ApplicationController def index @articles = Article.all authorize_action_for(@articles) end end
You can also check abilities in view templates:
link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)
Benefits of Authority
Authority is similar to Pundit in that it separates authorization logic into classes for each of your models. It is more configurable than Pundit. For example, you can easily create your own abilities.
It has many of the same benefits as Pundit compared with CanCanCan.
Drawbacks of Authority
The extra configurability does come with a price, I would say that Authority is more complicated to set up than Pundit and there is more boilerplate.
Declarative Authorization is another popular authorization gem. It is inspired by Role Based Access Control. I haven’t reviewed Declarative Authorization in this post because it doesn’t support Rails 5 and hasn’t been updated for a few years.
If I was starting a new Rails project in 2017 I would choose Pundit for authorization. Pundit is easy to maintain as the app grows and it helps the app to stay performant.
Authority is a close second. However, Pundit was easier to setup and has less configuration to maintain.