Securing controller endpoints is an important aspect of most non-trivial web applications. For Rails applications there are a number of established libraries used for this purpose. In this article we compare two of the most popular options, Pundit and CanCanCan. By means of an example we compare how these different libraries integrate into a Rails project.

Introduction

Most application developers will encounter the fundamental concepts of authentication and authorization pretty early in their career. These are essential components in securing a web application, or general software system, or indeed any system.

In this article I want to examine some of the popular options available for implementing authorization in a Rails project. I will consider the two most popular libraries, and look at how these authorization solutions could be incorporated into the ubiquitous PostsController. I hope to give you a feel for how each of the libraries fulfills its function, and weigh up the pros and cons of each.

Authentication and Authorization

Authentication describes how we verify the identity of the users of our system, i.e. who the user is. Some applications will expose functionality or content to anonymous users, in this case these users do not need to complete authentication. But many applications need to know who the user is. Authentication is often achieved using credentials like username and password, or a secret token.

Authorization describes what the user is allowed to do. The question of what the user can do will usually come after we know who the user is. So for most applications effective authorization will be dependent on authentication.

As a mature web framework, there are a number of different authentication solutions that will integrate with your Rails project. Common libraries include authlogic, clearance and devise. Each of these libraries make it easy for you to require that a user be logged-in before they can access a particular controller action, or endpoint. For example, from the clearance docs:

    
class ArticlesController < ApplicationController
  before_action :require_login

  def index
    current_user.articles
  end
end
    
  
Each of these authentication libraries make it relatively easy for the application developer to define that a controller action should only be accessible to a logged-in user, and the identity of that logged-in user can usually be retrieved via a current_user helper method, exposed within the controller.

This is what authentication gives us. But the real focus of this post is to look at the authorization options for Rails. As for authentication, there are several options available, but we will focus our consideration on the two most popular options:

Before we look at specifics for these particular gems, let's discuss what authorization is in a general sense, and why we need it.

Principles of authorization

Once our authentication system has identified who the user is, the next question is what should they be allowed to do. With Rails the public interface that your application exposes is defined by your routes file, typically found at config/routes.rb. One can apply constraints within the routes file, but for our purposes we consider that the routes file will simply map URL patterns, in a request, to the controller endpoints that should respond.

In this way we will consider the controller as the first point where our own application code will interact with the request. Once the request hits our controller action we want to ensure that the user has the proper credentials to access that action.

The system under consideration

Let's consider a concrete example which we will reuse during this article. The source code for all that follows can be found on GitHub. We will start with the standard rails project obtained by running rails new authorization-rails. On top of the standard project we will add the following components for our exploration.

We have a basic, unprotected PostsController which exposes index, show and destroy actions as follows:

    
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id].to_i)
  end

  def destroy
    @post = Post.find(params[:id].to_i)
    @post.destroy
    redirect_to posts_path, flash: { warning: "Post has been successfully deleted" }
  end
end
    
  
A corresponding resource will be added to the config/routes.rb file to expose these controller endpoints:
    
Rails.application.routes.draw do
  resource :session, only: [:update]

  resources :posts, only: [:index, :show, :destroy]

  …

end
    
As well as this standard route for the PostsController you can also see a small utility route for the SessionsController, which just allows us to toggle the logged-in user in our demo. To support our investigation of authorization options, we will stub out authentication with a simple User class that looks like this:
    
class User
  attr_reader :name, :is_admin
  USERS = []

  def initialize(name, is_admin=false)
    @name = name
    @is_admin = is_admin
    USERS << self
  end

  def self.jack
    User.find("jack")
  end

  def self.jill
    User.find("jill")
  end

  def self.find(name)
    USERS.find{ |u| u.name == name }
  end
end

User.new("jack", false)
User.new("jill", true)
    
The User model is simply made up of a name and a flag to indicate if the user is an admin. We also hardcode two users, Jack and Jill, where the latter is an admin.

We will store the current user's name in a session variable and offer a separate endpoint to allow us to toggle between the different users (SessionsController). The currently logged-in user will be exposed through a helper method on the base ApplicationController:

    
class ApplicationController < ActionController::Base
  prepend_before_action :set_current_user
  helper_method :current_user

  def current_user
    @current_user ||= User.find(session[:current_user].to_s)
  end

  private

  def set_current_user
    session[:current_user] ||= User.jack.name
  end
end
    
Exposing a current_user method to the controller, in this way, is a pretty standard technique among the authentication options.

Finally, in preparation for what follows we are going to add a couple more routes to config/routes.rb. These additional routes will provide namespaced-URLs to access equivalent PostsControllers, where the authorization has been implemented using pundit and cancancan, respectively:

    
Rails.application.routes.draw do
  resource :session, only: [:update]

  resources :posts, only: [:index, :show, :destroy]

  namespace :pundit do
    resources :posts, only: [:index, :show, :destroy]
  end

  namespace :can_can do
    resources :posts, only: [:index, :show, :destroy]
  end
end
    
Now let's take a look at how we can implement authorization for these controllers.

Pundit

Pundit provides a simple framework of helper methods, along with some conventions, which encourage the use of plain Ruby objects to define your authorization rules in an object-oriented fashion.

It requires that you create a policy file for each domain class that you wish to authorize actions against. This is a plain Ruby object that is instantiated with a user object and an instance of the class to which the policy relates. In our case we will construct a PostPolicy. Within the PostPolicy you will define separate instance methods that should return truthy or falsey values to indicate if the user has permission to execute the action on the defined Post instance.

In addition to this you can define a Scope class. The intention of this class is to encapsulate the logic that defines which instances the user should be allowed to list. This is typically defined as an inner class of the policy class. The Scope class should be initialized with the user instance and an initial scope which you will chain to, based on the particular user passed. The Scope class should then define a resolve method that will define the list of model instances that the defined user has access to.

Implementation

    
class PostPolicy
  attr_reader :user, :post

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

  def show?
    post.published ||
      user.is_admin ||
      (user.name == post.author)
  end

  def destroy?
    user.name == post.author
  end


  class Scope
    attr_reader :user, :scope

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

    def resolve
      if user.is_admin
        scope.all
      else
        scope.where(published: true).or(scope.where(author: user.name))
      end
    end
  end
end
  
The instance methods defined in the policy will use the User and Post instances to make the decision on whether the user should be allowed to execute the action on that model. The logic in these methods will reflect your business logic for the user and domain model involved. For example, in our case we have said that the user should be able to #show? the Post instance if the post is public or if the user is an admin or if the user is the author of the post.
    
  def show?
    post.published ||
      user.is_admin ||
      (user.name == post.author)
  end
    

By contrast the Scope does not concern itself with instances of the Post class, instead it uses the ActiveRecord query API to define which set of posts an individual user should be able to list. In our case the admin can list all posts, while other users should only see posts which are published, or which they have authored themselves.

    
    def resolve
      if user.is_admin
        scope.all
      else
        scope.where(published: true).or(scope.where(author: user.name))
      end
    end
    

We make use of this policy within the Pundit::PostsController. It exposes the same controller end-points as the original, unauthorized controller but this time we make use of the authorize helper-method included via Pundit. This method will instantiate an instance of our PostPolicy and will use that to decide if the current_user has access to the defined action on the defined model instance. When the user should not have access to the action then a Pundit::NotAuthorizedError is raised to abort the action.

In addition to the authorize method we can see the policy_scope helper method is being used in the index. This will be used to retrieve only the Post instances to which the current_user has access.

    
class Pundit::PostsController < ApplicationController
  include Pundit::Authorization

  def index
    @posts = policy_scope(Post)
  end

  def show
    @post = Post.find(params[:id].to_i)
    authorize @post
  end

  def destroy
    @post = Post.find(params[:id].to_i)
    authorize @post
    @post.destroy
    redirect_to pundit_posts_path, flash: { warning: "Post has been successfully deleted" }
  end
end
    
  

It can be instructive to imagine how the authorize helper-method might be implemented by Pundit. Something like this should provide the correct behaviour:

    
      def authorize(model, action)
        unless PostPolicy.new(current_user, model).public_send(action)
          raise Pundit::NotAuthorizedError
        end
      end
  
Obviously things are a little more complicated that this, as Pundit will need to infer the policy class from the model instance passed, also the action can be inferred from the controller action being executed. But these details aside, this is the essence of how Pundit enforces authorization using our simple policy classes.

Comments

This simple example shows some of the basics of how Pundit can be used to enforce authorization of your controller end-points. Some essential comments about this usage:

  • You will need to define a policy class for each domain class that you wish to authorize actions against. This can lead to a lot of new policy classes being required, but it encourages simple well-defined policies, which are easily tested.
  • Within your controller you will use authorize and policy_scope helper methods to authorize your actions and scope your queries, respectively.
  • Pundit also adds a method to your controllers called verify_authorized. This method will raise an exception if authorize has not yet been called, which might be a useful safety check in some cases.
  • We have looked at the methods used to secure your controller actions, but Pundit also offers a policy helper method which you can access in your views to conditionally display elements which the user should have access to. For example:
        
    <%- if policy(@post).edit? &>
      <%= link_to t('general.edit'), edit_blog_post_path(@post) %>
    <%- end %>
        
      

CanCanCan

This gem is an active community fork of the original cancan gem which is no longer maintained. I will use the shorter name to reference it from here on.

With CanCan we define a model called Ability that encapsulates all of the permissions that a user should have in your application. The initialize method on the Ability will take a user instance from your application, and uses a declarative syntax to define the different actions that this user is allow to execute on each of your domain models, specifically the can and cannot methods.

Within controllers (and views) there are are helper methods, like can? and authorize!. These methods will use an instance of the Ability class, parameterized with the current_user, and they will query this Ability model to determine if defined actions can be executed against particular model instances

Implementation

For our Post model the permissions can be declared in our Ability class as follows (found in app/models/ability.rb):
    
class Ability
  include CanCan::Ability

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

    return unless user.present?  # additional permissions for logged in users (they can read and destroy their own posts)
    can :read, Post, author: user.name
    can :destroy, Post, author: user.name

    return unless user.is_admin  # additional permissions for administrators
    can :read, Post
  end
end
    
We are capturing the same authorization logic in our Ability class as we did in our PostPolicy considered previously. In this case the can method is used to declare which actions the user should be able to access. The unauthenticated (anonymous) user can read any published posts, whilst an authenticated user can also read their own draft posts, or destroy any posts which they have authored. Finally an admin user can read any post without condition.

It should be noted that CanCan also supports declarations using the cannot(action, model) method. I avoid use of cannot declarations in order to keep this demonstration simple. Which is also, incidentally, the reason why I generally avoid its use in real projects.

Within the CanCan::PostsController the abilities of the current user can be accessed via the current_ability helper-method. The authorize!(action, model) method will use this current_ability to decide if the user has access to the defined action on the model instance. If the user does not have the requiste authorization then a CanCan::AccessDenied error is raised to abort the action.

    
class CanCan::PostsController < ApplicationController
  def index
    @posts = Post.accessible_by(current_ability)
  end

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

  def destroy
    @post = Post.find(params[:id].to_i)
    authorize! :destroy, @post
    @post.destroy
    redirect_to can_can_posts_path, flash: { warning: "Post has been successfully deleted" }
  end
end
    
  
The scoping of the query in the index action is achieved using the accessible_by scope on the model class, and passing the current_ability. This uses the existing rule declarations in your Ability class to build the SQL for retrieving the Post instances where the current_user has :index or :read access.

As we did for Pundit, it can be useful to imagine how the authorize! method might be implemented in CanCan:

    
      def authorize!(action, model)
        ability = Ability.new(current_user)
        unless ability.can?(action, model)
          raise CanCan::AccessDenied
        end
      end
  
This is a bit of a cheat because we have just used the built-in can? method on the Ability class. Nonetheless you can imagine that this method will need to iterate over the permissions registerd by the can declarations in the Ability initializer, and return true should any of these permissions match.

Comments

Again this simple PostsController example gives us a feel for how CanCan is integrated into your Rails project and how to apply authorization to your controller end points. Some key points:

  • By default authorization rules for all your domain models are declared in the Ability initializer. It can be useful to have all these rules managed in one place, but becomes unwieldy when you have a large number of domain models. There are documented techniques to split your ability file, but this is not the default pattern.
  • Within your controller you will typically use the authorize! helper method to protect each action. CanCan also provides a controller method load_and_authorize_resource which will run a before_action to instantiate the model instance and ensure that the current_user has permission to execute the action on the model instance. An exception is raised where the user is not authorized to carry out the action. This controller method can be useful to avoid repeated checks in each controller action, but you need to be sure that you have abilities defined that match your controller actions, in particular this can catch you out if you have any non-RESTful actions.
  • Again we have focussed on protecting our controller actions, however CanCan also provides the can? helper method which you can use in your views to conditionally show content based on the current_user's access permissions:
        
    <%- if can? :edit, @post %>
      <%= link_to t('general.edit'), edit_blog_post_path(@post) %>
    <%- end %>
        
      
  • The accessible_by magic doesn't work when you declare permissions logic using block syntax, it needs to be declared as a hash. Sometimes it is just easier to use the block syntax, so for me this is a real limitation of the accessible_by feature, and I generally tend to write an explicitly scoped query where this is needed.

Summary

In this article we have briefly introduced the concepts of authentication and authorization, and discussed how authentication (i.e. knowing who the user is) is often a precursor to authorization checks (i.e. checking what the user is allowed to do).

We then examined two of the most popular libraries for adding authorization to a Rails application, namely pundit and cancancan. We compared how these two libraries could be used to apply authorization checks to a simple controller.

With the Ability class CanCan offers a central place to declare the access permissions for your users, across all of your different domain models. It has a neat syntax for declaring rules, and for checking permissions in your controllers and views. As a result of these features, getting up and running with CanCan is pretty easy and it is a great choice if you have a relatively small number of domain models that you need to manage permissions for. However, as your number of domain models increases having a single Ability file to manage all the different permissions can get very complicated.

By contrast, Pundit takes a little more effort to get started. Each domain model that you wish to authorize against will need to have a policy class defined. These policies are plain Ruby objects, which are easy to digest and easy to test, which means that this system scales a lot better to more complicated domains. In addition, the Scope class for managing the querying of user-accessible objects is a lot more transparent than the accessible_by magic offered by CanCan, in my opinion.

Comments

There are no existing comments

Got your own view or feedback? Share it with us below …

×

Subscribe

Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.