Authorization with cancancan.

Cancancan is a gem used for Authorization in rails applications. It's very easy to use and offers a lot of flexibility.
In this article, I am going to explain how to use cancancan for Authorization when users have many roles.

Installation:

Add this to your gem file and run bundle install

 gem 'cancancan'

cancancan is continuation of the dead CanCan project.

Cancancan gem is independent of any authentication system. I used cancancan gem with devise gem.
After creating User model with devise and setting it up, it provides current_user method that returns 'current user' from session.
We will use the user returned from current_user method to define permissions.
All permissions are defined in a single location called Ability class.
If you haven't already generated Ability class, Please run the below generator. It will generates the Ability class in models folder.

rails g cancan:ability

After that we need a role model which will hold different role record. In my case role model only has one attribute i.e name
Lets create role model.

rails g model Role name:string

There were few default roles in our application, so I wrote the code in seed file to generate them.
My seed file looked like.

seed.rb

Role.find_or_create_by(id: 1, name: "admin")
Role.find_or_create_by(id: 2, name: "hr")
Role.find_or_create_by(id: 3, name: "associate")
Role.find_or_create_by(id: 4, name: "accountant")

Now we have to create a relation ship between user and role that users can have many roles and also roles can have many users.
We can either go for has_many through or has_and_belongs_to_many. We decided to go with has_and_belongs_to_many as we don't require the extra model in our case.

Below is the migration to create a table to establish an association between User and Model.

rails g migration create_roles_users

class CreateRolesUsers < ActiveRecord::Migration[5.0]
  def change
    create_table :roles_users, id: false do |t|
      t.belongs_to :role, index: true
      t.belongs_to :user, index: true
    end
  end
end

In user.rb add has_and_belongs_to_many :roles

class User < ApplicationRecord
  has_and_belongs_to_many :roles
end

In role.rb add has_and_belongs_to_many :users

class Role < ApplicationRecord
  has_and_belongs_to_many :users
end

We can add a role to user as.

user.roles << Role.find_by(name: "admin")
user.roles << Role.find_by(name: "hr")

We can also create a method that tells whether a user has a particular role or not.

def has_role?(role_sym)
  roles.any? { |r| r.name.underscore.to_sym == role_sym }
end

user.has_role? :admin
return true if user has a role with admin or return false
OR
we can create dynamic method to check the same.

 [:admin, :associate, :hr, :accountant].each do |role|
    define_method("#{role}?") { roles.exists?(name: role) }
  end

It generates admin?, associate?, hr? and accountant? methods.
user.admin? returns true if user has admin role.
I also added method in the User model to add a role to particular user, so that it can be used whenever required.

def add_role (role)
  unless send("#{role}?")
    roles << Role.find_by(name: role)
  end
end

Removing role was also a easy task. Below is the method which remove particular role from user.

def remove_role(role)
  if send("#{role}?")
    roles.delete(Role.find_by(name: role))
  end
end

Now in ability class (ability.rb), we will define permissions. For defining permission we use initialize method inside ability class.

class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new 

    if user.admin? 
      can :manage, :all # user can perform any action on any object
    elsif user.hr? 
      can :manage, SalaryDocument
    else
      can :read, :all # user can read any object
    end
  end
end

can :manage, SalaryDocument it means a user with hr role can able to manage(perform CRUD operations) SalaryDocument.
The current user's permissions can then be checked using the can? and cannot? methods in the views and controllers.

if can? :update, Article
%li= link_to 'Edit', edit_salary_document_path

But user can still go to edit page by url, so we need a way to restrict it, if any user do not have access to that resource.
We can do that by using authorize! method. This method in the controller will raise an exception if the user is not able to perform the given action.

def edit
  @article = Article.find(params[:id])
  authorize! :update, @article
end

Setting this for every action can be tedious, therefore we can use load_and_authorize_resource method, that is provided by cancancan to automatically authorize all RESTful actions.
It will use a before action to load the resource into an instance variable and authorize it for every action.
load_and_authorize_resource is nothing but a before action which is run before each action.

class SalaryDocumentsController < ApplicationController
load_and_authorize_resource
  def show
    # @salary_document instance variable is already instantiated by load_and_authorize_resource
  end
end

When using strong_parameters, you have to sanitize inputs, before saving the record in actions such as :create and :update.
For the :create action, Cancancan will initialize the Model object and authorize the resource but will not add a record automatically, so the typical usage would be something like:

class SalaryDocumentsController < ApplicationController
  load_and_authorize_resource
  
  def create
    if @salary_document.save
      redirect_to salary_documents_path, notice: "The file is uploaded"
    else
      render :new
    end
  end
  
  private 
  def create_params
    params.require(:salary_document).permit(:name, :month, :year, :document)
  end 
end

You can also skip any action when using load_and_authorize_resource using skip_authorize_resource method.

class SalaryDocumentsController < ApplicationController
  load_and_authorize_resource
  skip_authorize_resource only: :index
  
  ...
end

If the user authorization fails, a CanCan::AccessDenied exception will be raised. You can catch this and modify its behavior in the ApplicationController.

class ApplicationController < ActionController::Base
  ...
  rescue_from CanCan::AccessDenied do |_exception|
    redirect_to root_url, notice: "Access Denied"
  end
end

We can use check_authorization in ApplicationController to ensure authorization happens on every action in our application.

class ApplicationController < ActionController::Base
  check_authorization
end

If we want to skip check_authorization on any controller subclass, then we can add skip_authorization_check to that controller, It will skip the authorization.

Hope this article helps. Thanks