header-sample
Technology Software Ruby on Rails Toolset Authorization
May 29th, 2019 - Dave Strock

Rails Authorization: Comparing CanCan and Pundit

CanCan was an authorization library created in the early days of Rails by Ryan Bates. It gained wide adoption as a way to organize access control to model objects based on user, but then fell into disrepair in Bates' absence.

To modernize the library, a fork called CanCanCan was created. Many of the issues of compatibility with modern libraries were fixed, but the main design of the library was retained. As CanCan, or CanCanCan, was floundering and because of perceived design issues, an alternative — Pundit — was developed.

Below is a brief overview of and benefit comparison between the two libraries.

A Closer Look At CanCanCan

CanCanCan uses a single Ability class to manage all authorizations, which you can create with the Rails generator:


  rails generate cancan:ability
            

This creates app/models/ability.rb which looks like this:


  class Ability
    include CanCan::Ability

    def initialize(user)
    end
  end
            

You must then fill the initialize method with declarations of what actions can (or cannot) be performed on each model by each user type. These authorizations can be scoped to specific conditions by passing a third parameter to the can method containing a hash of model attributes and values that must be present in the record to gain the ability:


  class Ability
    include CanCan::Ability

    def initialize(user)
      can :read, Post, public: true

      if user.present?  # additional permissions for logged in users (they can read their own posts)
        can :read, Post, user_id: user.id

        if user.admin?  # additional permissions for administrators
          can :read, post
        end
      end
    end
  end

You can then check abilities in the view to conditionally draw certain aspects of the UI:


   <% if can? :read, @post %>
     <%= link_to "View", @post %>
   <% end %>
            

In controllers, you can use the authorize! method, which will raise an exception if the user, determined by calling the current_user method, is not authorized to perform the given action.


  class PostsController < ApplicationController
    def show
      @post = Post.find(params[:id])
      authorize! :read, @post
    end
  end
            

Since an exception is thrown when access is denied, you need to catch it and respond appropriately, which is usually done in the ApplicationController:


  class ApplicationController < ActionController::Base
    rescue_from CanCan::AccessDenied do |exception|
      respond_to do |format|
        format.json { head :forbidden, content_type: 'text/html' }
        format.html { redirect_to main_app.root_url, notice: exception.message }
        format.js   { head :forbidden, content_type: 'text/html' }
      end
    end
  end
            
Assessment

Nothing about CanCanCan is particularly problematic, however it’s lacking in a few ways. The most obvious is that as the authorization needs of the application grow, the Ability class starts to become unwieldy and there aren't ways to organize things well.

A Closer Look At Pundit

Pundit was designed to be a more object-oriented approach to authorization, using plain old ruby objects. This allows developers to use all the normal code organization patterns, modules, composition, inheritance, etc. Instead of a single Ability class, Pundit starts with a policy class per-model, stored in the app/policies/ directory.


  class PostPolicy
    attr_reader :user, :post

    def initialize(user, post)
      @user = user
      @post = post
    end

    def update?
      user.admin? || !post.published?
    end
  end
            

To initialize Pundit support, you need to include it in the ApplicationController:


  class ApplicationController < ActionController::Base
    include Pundit
  end
  

This gives you access to the authorize method that is similar to CanCan's authorize!:



  class PostsController < ApplicationController
    def update
      @post = Post.find(params[:id])
      authorize @post
      if @post.update(post_params)
        redirect_to @post
      else
        render :edit
      end
    end
  end
  

Pundit infers both that the object of class Post will have a corresponding PostPolicy and that the update action should check the update? method of the policy. If the names don't match, you can pass them explicitly:


  class PostsController < ApplicationController
    def publish
      @post = Post.find(params[:id])
      authorize @post, :update?
      @post.publish!
      # ...
    end
  end
                          

Checking policy in views is similar to CanCan, but you call the methods you define directly instead of passing a symbolized representation of the ability:


  <% if policy(@post).update? %>
    <%= link_to "Edit post", edit_post_path(@post) %>
  <% end %>
         

Notice this is just a plain old Ruby object. It just needs to receive a user and resource object as initialization parameters, and provide methods that check authorization. This allows trivially breaking free of ActiveRecord, as well as the constraint requiring every ability to manage only a single database table. Let's say you want to control abilities on a dashboard that is not backed directly by a model class. That's fine — you can pass any object into a policy initializer:


  class DashboardPolicy < Struct.new(:user, :dashboard)
    def show_super_secret?
      user.id == 42
    end
  end

    # In view
  <%= if policy(:dashboard).show_super_secret? %>
    <%= link_to 'Super Secret Dashboard', secret_dashboard_path %>
  <% end %>

    # In controller
  class DashboardsController < ApplicationController
    def secret_dashboard
      authorize :dashboard, :show_super_secret?
      # ...
    end
  end

              
Assessment

Since Pundit manages to provide the same feature set as CanCan with a more object-oriented style, we choose Pundit when possible.


Contact Us


Have questions? Reach out. We're happy to chat about who we are, what we do, and how we can partner with your organization to better utilize custom software in achieving your business goals. Drop us a line and let's chat.