Ruby on Rails

Authorization with CanCan

Introduction#

CanCan is a simple authorization strategy for Rails which is decoupled from user roles. All permissions are stored in a single location.

Remarks#

Before using CanCan don’t forget to create Users either by devise gem or manually. To get maximum functionality of CanCan do create an Admin user.

Getting started with CanCan

CanCan is a a popular authorization library for Ruby on Rails which restricts user access to specific resources. The latest gem (CanCanCan) is a continuation of the dead project CanCan.

Permissions are defined in the Ability class and can be used from controllers, views, helpers, or any other place in the code.

To add authorization support to an app, add the CanCanCan gem to the Gemfile:

gem 'cancancan'

Then define the ability class:

# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
  end
end

Then check authorization using load_and_authorize_resource to load authorized models into the controller:

class ArticlesController < ApplicationController
  load_and_authorize_resource

  def show
    # @article is already loaded and authorized
  end
end

authorize! to check authorization or raise an exception

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

can? to check if an object is authorized against a particular action anywhere in the controllers, views, or helpers

<% if can? :update, @article %>
  <%= link_to "Edit", edit_article_path(@article) %>
<% end %>

Note: This assumes the signed user is provided by the current_user method.

Defining abilities

Abilities are defined in the Ability class using can and cannot methods. Consider the following commented example for basic reference:

class Ability
  include CanCan::Ability

  def initialize(user)
    # for any visitor or user
    can :read, Article

    if user
      if user.admin?
        # admins can do any action on any model or action
        can :manage, :all
      else
        # regular users can read all content
        can :read, :all
        # and edit, update and destroy their own user only
        can [:edit, :destroy], User, id: user_id
        # but cannot read hidden articles
        cannot :read, Article, hidden: true
      end
    else
      # only unlogged visitors can visit a sign_up page:
      can :read, :sign_up
    end
  end
end

Handling large number of abilities

Once the number of abilities definitions start to grow in number, it becomes more and more difficult to handle the Ability file.

The first strategy to handle these issue is to move abilities into meaningful methods, as per this example:

class Ability
  include CanCan::Ability

  def initialize(user)
    anyone_abilities 

    if user
      if user.admin?
        admin_abilities
      else
        authenticated_abilities
      end
    else
      guest_abilities
    end
  end

  private
  
  def anyone_abilities
    # define abilities for everyone, both logged users and visitors
  end

  def guest_abilities
    # define abilities for visitors only
  end

  def authenticated_abilities
    # define abilities for logged users only
  end

  def admin_abilities
    # define abilities for admins only
  end
end

Once this class grow large enough, you can try breaking it into different classes to handle the different responsibilities like this:

# app/models/ability.rb
class Ability
  include CanCan::Ability

  def initialize(user)
    self.merge Abilities::Everyone.new(user)

    if user
      if user.admin?
        self.merge Abilities::Admin.new(user)
      else
        self.merge Abilities::Authenticated.new(user)
      end
    else
      self.merge Abilities::Guest.new(user)
    end
  end
end

and then define those classes as:

# app/models/abilities/guest.rb
module Abilities
  class Guest
    include CanCan::Ability

    def initialize(user)
      # Abilities for anonymous visitors only
    end
  end
end

and so on with Abilities::Authenticated, Abilities::Admin or any other else.

Quickly test an ability

If you’d like to quickly test if an ability class is giving the correct permissions, you can initialize an ability in the console or on another context with the rails environment loaded, just pass an user instance to test against:

test_ability = Ability.new(User.first)
test_ability.can?(:show, Post) #=> true
other_ability = Ability.new(RestrictedUser.first)
other_ability.cannot?(:show, Post) #=> true

More information: https://github.com/ryanb/cancan/wiki/Testing-Abilities


This modified text is an extract of the original Stack Overflow Documentation created by the contributors and released under CC BY-SA 3.0 This website is not affiliated with Stack Overflow