Development icon

We recently needed to implement a fairly standard user authentication / permission system on a Rails 3 project.

Steven Merrill, Director of Devops
#Development | Posted

We recently needed to implement a fairly standard user authentication / permission system on a Rails 3 project, with the ability to define these permissions using roles so that it could easily be centralized and expanded on (ie with new roles and permissions down the line). While rolling your own custom code is always an option, there is already a robust collection of gems to do exactly this. This blog post quickly presents how to combine three of these to implement this system.

Devise

Devise is a modular user authentication system thcgem ‘cancan’

  • Generate the Ability class which will contain your user permissions:

    rails g cancan:ability

  • Open up this file and add the user line so that you can tie your abilities to the currently logged-in user:

    # app/models/ability.rb

    class Ability

      include CanCan::Ability

     

      def initialize(user)

        # Define abilities for the passed in user here.

        user ||= User.new # guest user (not logged in)

        # a signed-in user can do everything

        if user_signed_in?

          can :manage, :all

        end

      end

    end

Your application controller will need to know what to do if a CanCan exception is thrown:

# app/controllers/application_controller.rb

...

  rescue_from CanCan::AccessDenied do |exception|

    redirect_to root_url, :alert => exception.message

  end

...

Now you can decide which actions require CanCan authorization:

# app/controllers/documents_controller.rb

...

  def show

   @document = Document.find(params[:id])

   # require the ability to read documents

   authorize! :read, @document

  end

Adding this manually to every action in every controller is obviously a dumb idea, so you can just use load_and_authorize_resource at the top of your controller to authorize all actions in a RESTful fashion.

At this point you’ll want to actually make this more granular and allow abilities to be based on roles, which brings us to Role Model.

Role Model

The CanCan documentation provides a very nice explanation of the basic concept of using a bitmask to represent many roles per user, and Role Model is a ready-to-go solution if this approach fits your needs.

  • Add the role_model gem to your gemfile and run bundle install.
  • Edit your user.rb to make it aware of Role Model. With the existing Devise stuff, it should look more or less similar to this:

    # app/models/user.rb

    require 'role_model'

     

    class User < ActiveRecord::Base

     

      # Include default devise modules. Others available are:

      # :token_authenticatable, :confirmable,

      # :lockable, :timeoutable and :omniauthable

      devise :database_authenticatable, :registerable,

             :recoverable, :rememberable, :trackable, :validatable

     

      include RoleModel

     

      # Setup accessible (or protected) attributes for your model

      attr_accessible :email, :password, :password_confirmation, :remember_me, :roles, :roles_mask

     

      # optionally set the integer attribute to store the roles in,

      # :roles_mask is the default

      roles_attribute :roles_mask

     

      # declare the valid roles -- do not change the order if you add more

      # roles later, always append them at the end!

      roles :admin, :editor, :guest

We effectively defined 3 roles now, now we need to update our ability.rb model to tie our permissions to them.

# app/models/ability.rb

class Ability

  include CanCan::Ability

 

  def initialize(user)

    # Define abilities for the passed in user here.

    user ||= User.new # guest user (not logged in)

    # a signed-in user can do everything

    if user.has_role? :admin

     # an admin can do everything

      can :manage, :all

    elsif user.has_role? :editor

      # an editor can do everything to documents and reports

      can :manage, [Document, Report]

      # but can only read, create and update charts (ie they cannot

      # be destroyed or have any other actions from the charts_controller.rb

      # executed)

      can [:read, :create, :update], Chart

      # an editor can only view the annual report

      can :read, AnnualReport

    elsif user.has_role? :guest

      can :read, [Document, Report, Chart]

    end

  end

end

This is a really simple demonstration of what can be done fairly quickly with this stack, and in our application it was extended to leverage user-level custom permissions for specific assets and very fine-tuned abilities with conditional displays of specific view segments.

Steven Merrill

Director of Devops