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