Dynamic roles and permissions using cancan

Most of us already use cancan for authorization, where we define the Role Based Access (RBAC) to various models in the Ability class. However, any changes to these abilities, requires code changes to the Ability class and restarting the application for the changes to take effect.

What if we could assign these permissions dynamically? That would be great, wouldn’t it. This is how we manage roles and permissions dynamically. Some initial thoughts were picked up from here

This approach is based primarily on authorization for the controller actions.

Let’s consider an example. We will add the models as they are required. Right now the basic application has models User, Role and Permission. The relationship is as shown

Role #the model to save the role
  :name                 # the role name
  :has_many :users
  :has_and_belongs_to_many :permissions

User
  :name       # user name
  :email      # user email
  :password   # user password
  :belongs_to :role

Permission       # the model to save the permission
  :subject_class # model names like User, Role, Book, Author
  :action        # controller action like new, create or destroy

Firstly, in the application controller, we need to define protected methods.


class ApplicationController < ActionController::Base
  protect_from_forgery
  rescue_from CanCan::AccessDenied do |exception|
    flash[:alert] = "Access denied. You are not authorized to access the requested page."
    redirect_to root_path and return
  end

  protected

  #derive the model name from the controller. egs UsersController will return User
  def self.permission
    return name = self.name.gsub('Controller','').singularize.split('::').last.constantize.name rescue nil
  end

  def current_ability
    @current_ability ||= Ability.new(current_user)
  end

  #load the permissions for the current user so that UI can be manipulated
  def load_permissions
    @current_permissions = current_user.role.permissions.collect{|i| [i.subject_class, i.action]}
  end
end

If you have defined the controller for some other model like InvitesController for Invitation model or have a controller defined in a namespace, you can override the method as

# app/controllers/invites_controller.rb

private

def self.permission
  return "Invitation"
end

or

def self.permission
  return "Namescope::Model"
end

Now, we create a rake task that finds all the controllers and creates permissions for each  public method of the controller. This rake task handles the controllers in a namespace too.


namespace 'permissions' do
  desc "Loading all models and their related controller methods inpermissions table."
  task(:permissions => :environment) do
    arr = []
    #load all the controllers
    controllers = Dir.new("#{Rails.root}/app/controllers").entries
    controllers.each do |entry|
      if entry =~ /_controller/
        #check if the controller is valid
        arr << entry.camelize.gsub('.rb', '').constantize
      elsif entry =~ /^[a-z]*$/ #namescoped controllers
        Dir.new("#{Rails.root}/app/controllers/#{entry}").entries.each do |x|
          if x =~ /_controller/
            arr << "#{entry.titleize}::#{x.camelize.gsub('.rb', '')}".constantize
          end
        end
      end
    end

    arr.each do |controller|
      #only that controller which represents a model
      if controller.permission
        #create a universal permission for that model. eg "manage User" will allow all actions on User model.
        write_permission(controller.permission, "manage", 'manage') #add permission to do CRUD for every model.
        controller.action_methods.each do |method|
          if method =~ /^([A-Za-z\d*]+)+([\w]*)+([A-Za-z\d*]+)$/ #add_user, add_user_info, Add_user, add_User
            name, cancan_action = eval_cancan_action(method)
            write_permission(controller.permission, cancan_action, name)
          end
        end
      end
    end
  end
end

#this method returns the cancan action for the action passed.
def eval_cancan_action(action)
  case action.to_s
  when "index"
    name = 'list'
    cancan_action = "index" <strong>#let the cancan action be the actual method name</strong>
    action_desc = I18n.t :list
  when "new", "create"
    name = 'create and update'
    cancan_action = "create"
    action_desc = I18n.t :create
  when "show"
    name = 'view'
    cancan_action = "view"
    action_desc = I18n.t :view
  when "edit", "update"
    name = 'create and update'
    cancan_action = "update"
    action_desc = I18n.t :update
  when "delete", "destroy"
    name = 'delete'
    cancan_action = "destroy"
    action_desc = I18n.t :destroy
  else
    name = action.to_s
    cancan_action = action.to_s
    action_desc = "Other: " < cancan_action
  end
  return name, cancan_action
end

#check if the permission is present else add a new one.
def write_permission(model, cancan_action, name)
  permission = Permission.find(:first, :conditions => ["subject_class = ? and action = ?", model, cancan_action])
  unless permission
    permission = Permission.new
    permission.name = name
    permission.subject_class = model
    permission.action = cancan_action
    permission.save
  end
end

Finally, our Ability class needs to be defined as

# app/models/ability.rb

class Ability
  include CanCan::Ability

  def initialize(user)
    user.role.permissions.each do |permission|
      if permission.subject_class == "all"
        can permission.action.to_sym, permission.subject_class.to_sym
      else
        can permission.action.to_sym, permission.subject_class.constantize
    end
  end
end

Remember, all the controllers need to have the before_filters configured to authorize the resources.

before_filter :load_and_authorize_resource
before_filter :load_permissions # call this after load_and_authorize else it gives a cancan error

In most cases, the roles are already decided, so we can add them through the seed file. We need at least one user that can assign permissions to other users, the ‘Super Admin’. Here we have created 2 users with different roles.


 #the highest role with all the permissions.
 Role.create!(:name => "Super Admin")

 #other role
 Role.create!(:name => "Staff")

 #create a universal permission
 Permission.create!(:subject_class => "all", :action => "manage")

 #assign super admin the permission to manage all the models and controllers
 role = Role.find_by_name('Super Admin')
 role.permissions << Permission.find(:subject_class => 'all', :action => "manage")

 # create a user and assign the super admin role to him.
 user = User.new(:name => "Prasad Surase", :email => "prasad@joshsoftware.com", :password => "prasad", :password_confirmation => "prasad")
 user.role = role
 user.save!

User.create(:name => "Neo", email => "neo@matrix.com", :password => "the_one", :password_confirmation => "the_one", :role_id => Role.find_by_name('Staff').id)

Neo is just one of the members on my staff 😉 Now, seed the database so that the roles and users are created and remember to run the rake task to create the permissions automatically for existing controllers actions.

Suppose we want to assign the permissions from our application, we need to add a controller. So, we create RolesController with CRUD operations. So, the RolesController looks like this


class RolesController < ApplicationController
  #devise so that only logged-in user can access
  before_filter :authenticate_user!

  #only user with super admin role can access
  before_filter :is_super_admin?

  def index
    @roles = Role.all
  end

  def edit
    @role = Role.find(params[:id])
    @permissions = Permission.all
  end

  def update
    #assign the permissions to the role if it isnt already assigned.
  end

  private

  def is_super_admin?
    redirect_to home_path and return unless current_user.super_admin?
  end

end

Suppose we have some models on which we want to perform CRUD operations, we create their corresponding controllers. For example

Part
  has_many :drawings

Drawing
  belongs_to :part

If these controllers have been added after our initial rake task (for adding permissions) has been run, we need to run the rake task again so that all the CRUD operations for these controllers will be loaded in the permissions list.

Lets consider a scenario where the permissions for Drawing depend upon the permissions for Part. For example, a User (i.e. its corresponding Role) can create drawing if he has permission to create a Part. Similarly, the user can update and delete a Drawing if he has permission to update a Part.


class RolesController < ApplicationController
  before_filter :authenticate_user!
  before_filter :is_super_admin?

  def index
    #you dont want to set the permissions for Super Admin.
    @roles = Role.all.keep_if{|i| i.name != "Super Admin"}
  end

  def show
    @role = Role.find(params[:id])
    @permissions = @role.permissions
  end

  def edit
    @role = Role.find(params[:id])
    #we dont want the Drawing permissions to be displayed.
    #this way u can display only selected models. you can choose which methods u want to display too.
    @permissions = Permission.all.keep_if{|i| ["Part"].include? i.subject_class}.compact
    @role_permissions = @role.permissions.collect{|p| p.id}
  end

  def update
    @role = Role.find(params[:id])
    @role.permissions = []
    @role.set_permissions(params[:permissions]) if params[:permissions]
    if @role.save
      redirect_to roles_path and return
    end
    @permissions = Permission.all.keep_if{|i| ["Part"].include? i.subject_class}.compact
    render 'edit'
  end

  private

  def is_super_admin?
    redirect_to root_path and return unless current_user.super_admin?
  end
end

The Role model looks like this

# app/models/role.rb

def set_permissions(permissions)
  permissions.each do |id|
    #find the main permission assigned from the UI
    permission = Permission.find(id)
    self.permissions << permission
    case permission.subject_class
      when "Part"
        case permission.action
          #if create part permission is assigned then assign create drawing as well
          when "create"
            self.permissions << Permission.where(subject_class: "Drawing", action: "create")
          #if update part permission is assigned then assign create and delete drawing as well
          when "update"
            self.permissions << Permission.where(subject_class: "Drawing", action: ["update", "destroy"])
          end
        end
      end
    end

Using the above method in Role model we can assign a number of permissions related to different models that come with the basic permissions. Of course, it all depends upon your application flow too. For example, some Drawing permissions are based on a particular Part permission.

The advantage of this approach is that SuperAdmin can assign permissions from a web UI. These permissions are loaded in the before_filter and can be used to dynamically alter the authorization for different users as per their roles. Also, admin can create a Role dynamically and assign any combination of permissions to it.

The disadvantage of this approach is, if the controllers and models list grows too large, the permissions list grows large too. Also, the user who is going to assign the permissions (in this case the Super Admin) needs to have complete knowledge of all the methods of all the controllers since a unassigned permission may have a functionality effect. But then, it is an acceptable assumption that the Admin knows what he is doing.

I have created a github repo dynacan (dynamic cancan) for you to play around with. You can clone and run it and see the actual working. Do send me questions and queries and I can improve it. I also aim to make this a gem soon, so if you require dynamic permissions, it can be added to the Gemfile.

Advertisements
This entry was posted in Ruby on Rails and tagged , , , . Bookmark the permalink.

40 Responses to Dynamic roles and permissions using cancan

  1. When trying to implement this I continually get “undefined method `permissions’ for nil:NilClass” from the following line of code located in ability.rb “current_user.role.permissions.each do |permission|”

    • Prasad Surase says:

      You cant access current_user in any model. its accessible in controller only, provided you have specified ‘before_filter: authenticate_user!’. In ability model, its ‘user’ and not ‘current_user’.

      • thomasjosephbush says:

        Thanks, I was trying out current user as I thought user was the portion returning nil. It was actually role that was returning nil, there was a mistake in my db, so role never got saved to a user. Got it working, thanks!

  2. Prasad Surase says:

    My pleasure 🙂

  3. Ruben says:

    I have a little doubt… why when I try to enter into parts index the server redirects to the home/index?. I don’t see any redirect_to_root_path o anything like that in parts_controller.rb

    • from a quick glance looks like its right here:

      def is_super_admin?
      redirect_to root_path and return unless current_user.super_admin?
      end

      So your system thinks your not a super admin maybe?

  4. kaleem says:

    Hi,
    I have creating same above application (i.e Authentication, Roles & Permission)using with can can but without using devise gem . Here I am getting some issue which is mentioned below:-
    (application_controller.rb)
    (1)
    protected

    def self.permission
    return name = self.name.gsub(‘Controller’,”).singularize.split(‘::’).last.constantize.name rescue nil
    end

    def current_ability
    @current_ability ||= Ability.new(current_user)
    end

    def load_permissions
    @current_permissions = current_user.role.permissions.collect{|i| [i.subject_class, i.action]}
    end

    ____________________________________
    (1) How can get current user in current_ability function?
    def current_ability
    @current_ability ||= Ability.new(current_user)
    end
    (2) How can load permission for current user’s role.
    def load_permissions
    @current_permissions = current_user.role.permissions.collect{|i| [i.subject_class, i.action]}
    end

    Please help me what’s going on here.

    Thanks & regards

    • 1) the current_user is a helper method provided by devise. If your devise based model is Actor, the helper will be currect_actor.
      2) the method ‘self.permissions’ returns the model name that the controller is written for. We usually have a UsersController to handle User model. But if u have HumansController for User model, then simply need to return ‘User’ from the method

      Note: If you arent using any authentication mechanism, how are you planning to restrict the user from accessing certain actions.

      • kaleem says:

        Hi Thanks a lot for reply Prasad sir
        Actually I am new in Ruby On Rails. I hope you will suggest me for right direction for this project…

        I am creating a small app in which admin can restrict to users for:-
        (1) Assign roles & permission on particular module etc. Admin can manage whole the things (i.e role & permission) from back-end.
        (2) Authorized user can access particular module.

        Please reply sir.

      • You need to learn Rails first. A simple app will help. follow the rails guides.

  5. kannan says:

    namespace ‘permissions’ do
    desc “Loading all models and their related controller methods inpermissions table.”
    task(:permissions => :environment) do
    arr = []
    #load all the controllers
    controllers = Dir.new(“#{Rails.root}/app/controllers”).entries
    controllers.each do |entry|
    if entry =~ /_controller/
    #check if the controller is valid
    arr << entry.camelize.gsub('.rb', '').constantize
    elsif entry =~ /^[a-z]*$/ #namescoped controllers
    Dir.new("#{Rails.root}/app/controllers/#{entry}").entries.each do |x|
    if x =~ /_controller/
    arr << "#{entry.titleize}::#{x.camelize.gsub('.rb', '')}".constantize
    end
    end
    end
    end

    arr.each do |controller|
    #only that controller which represents a model
    if controller.permission
    #create a universal permission for that model. eg "manage User" will allow all actions on User model.
    write_permission(controller.permission, "manage", 'manage') #add permission to do CRUD for every model.
    controller.action_methods.each do |method|
    if method =~ /^([A-Za-z\d*]+)+([\w]*)+([A-Za-z\d*]+)$/ #add_user, add_user_info, Add_user, add_User
    name, cancan_action = eval_cancan_action(method)
    write_permission(controller.permission, cancan_action, name)
    end
    end
    end
    end
    end
    end

    #this method returns the cancan action for the action passed.
    def eval_cancan_action(action)
    case action.to_s
    when "index"
    name = 'list'
    cancan_action = "index" #let the cancan action be the actual method name
    action_desc = I18n.t :list
    when “new”, “create”
    name = ‘create and update’
    cancan_action = “create”
    action_desc = I18n.t :create
    when “show”
    name = ‘view’
    cancan_action = “view”
    action_desc = I18n.t :view
    when “edit”, “update”
    name = ‘create and update’
    cancan_action = “update”
    action_desc = I18n.t :update
    when “delete”, “destroy”
    name = ‘delete’
    cancan_action = “destroy”
    action_desc = I18n.t :destroy
    else
    name = action.to_s
    cancan_action = action.to_s
    action_desc = “Other: ” [“subject_class = ? and action = ?”, model, cancan_action])
    unless permission
    permission = Permission.new
    permission.name = name
    permission.subject_class = model
    permission.action = cancan_action
    permission.save
    end
    end

    i need clear explanation of this code please…?

    • kannan, please have a look at how cancan works. you can understand that through documentation.
      1) the rake task loads all the public methods of the controllers that represent any model and where you need authorization to take place.
      2) the ‘eval_cancan_action’ method evaluates as to what permission u want to allow the assign to that method. eg. ‘show’ is represented as ‘view’ in user understandable language. also, we usually assume that user should be able to ‘create’ a object. but, in rails, it maps to two actions (new and create). here u can check that if the action is ‘create’ allow access to actual new and create methods.

  6. I cannot figure out where the code of the namespace permissions exists in this sample https://github.com/prasadsurase/dynacan/tree/master/app

  7. I’m trying to apply this code in my project!.
    and all is well, but I faced one bug.

    the bug is any action otherwise :manage, :all doesn’t work , i.e. the if condition for can? :show, @item doesn’t work despite I’ve configured the permissions in the database for the current user as ( :show and Item

    This links would display the issue.
    http://www9.0zz0.com/2014/03/03/15/143910424.png the vistor view ( create link ) must appear. ( this’s the bug)
    http://www9.0zz0.com/2014/03/03/15/486235854.png permission table
    http://www9.0zz0.com/2014/03/03/15/322991563.png visitor permissions

    I cannot figure out where the problem is.
    please help.

    • Problem solved, I was not coding the view the right way as the sample you put, it must be if @current_permissions.include?([“Item”, “show”] otherwise using if can? “Item”,”show”.

      thanks alot for this useful tutorial, prasad.

      • Hi, as I’ve found another solution so I found I must write it here for you and for others,

        in your view you wrote a if @current_permissions.include?([“Item”, “show”])
        **but you can use the normal cancan gem form — (( if can? :show, Item )) and it will work properly for all the permissions like (manage, all … so on ).

  8. Rajesh Gurbani says:

    Hello,

    I am using rails 4 + cancan

    Q. How to give authorize_resource with multiple models on single controller ?

    Example :

    ability.rb

    if user.is_sys_owner?
    can :manage, [MdmArea, MdmCity, MdmCountry, MdmState]
    end

    class Admin::MdmController [‘MdmCountry’, ‘MdmCity’, ‘MdmArea’, ‘MdmState’] # Not Working?
    #Code
    end

    Any Solution please suggest?
    Thanks

  9. thanks for this article, first for this line role.permissions < ‘all’, :action => “manage”) it was raising unknown key subject_class , so i changed it to this role.permissions << Permission.find_by_subject_class_and_action('all','manage')

    second i have namespaces in my application Admin , Employee , Student. so my controllers like this Employee::TicketsController < EmployeeController , Note that EmployeeController inherits from ApplicationController. so i added the same code you have for ApplicationController inside my ApplicationController (is that true? ) , and what about the subject_class , you said in case of namespaces we have to use
    private
    def self.permission
    return "Namescope::Model"
    end

    so i have this inside Employee::TicketsController
    private
    def self.permission
    return "Employee::Tickets"
    end

    but it saved inside Permission Model , subject_class as Employee::Tickets , not the model name itself. i am not sure if thats correct so i need to know before proceeding if i am correct or i will need some modifications ?

  10. Radha says:

    Hi i have downloaded the code from Github. But when i try to create the user i am getting the error “NoMethodError in HomeController#index”
    “app/controllers/application_controller.rb:21:in `load_permissions'”
    How to solve it ?

  11. edddzgeddie says:

    Hi Prasad, I love this! Thanks for sharing!
    Any suggestions on how one may implement Role Inheritance? I’d like to assign all permissions from one Role to another, by selecting the Role itself. Maybe you can point me in the right direction.

    BRGDS

    • I am glad that you liked the post. As for your question, its not inheritance considering the relationship that exists between Role and Permission. Inheritance is used when you have multiple classes where a class inherits some features of the base class. As for assigning the permissions of a role to another role, you could simply create a instance method for a role that accepts another role and assigns the permissions to the role object on whom the method is called. Or you can create a service object that accepts the two roles during initialisation and assigns the permissions from first to the second role.

  12. Jascha says:

    Hey,

    I am a rails starter and I tried to follow your guide but somehow I get the error “undefined local variable or method `load_and_authorize_resource’ for #” as soon as I implement `load_and_authorize_resource’ in my controllers.

    Further I need to define different roles for different IDs my objects. I tried to alter the permissions.rake code but I dont understand fully what it’s for and what the code means so I’ve probably done it the wrong way.

    Would appreciate some help here!

    Regards!

    • Jascha, you dont need to define the ‘load_and_authorize_method’. Its a helper method provided by cancan. Please checkout the cancan gem.

      • Jascha says:

        Okay, but since when can I use helper methods in my controller? Since I am using Rails 4 I got “cancancan” but that shouldnt be the problem because they provide exactly the same method set. Really dont know what it could cause.

      • Jascha says:

        Ah got it! For people facing the same problem:

        On the current version of ‘cancancan’ you only use `load_and_authorize_resource` and not `before_action load_and_authorize_resource`!

        Thanks anyway Prasad.

      • Jascha says:

        Could you tell me what I have to change if I need to include subject_ids in my permissions? Parts of my model structure look like this:

        Department.rb
        has_many :subs

        Sub.rb
        belongs_to :department
        has_one :page

        Page.rb
        belongs_to :sub

        So users from a certain department have to have permissions to see everything of that department. Is that possible with your code?

        Regards,
        Jascha.

  13. lambi says:

    I try to put in place a code which allows to dynamically manage permissions like this post. This works, but I’m stuck at the owners permission.

    Here is my code (not the best code), I tried to play with the “reflect_on_all associations” but I did not succeed, http://stackoverflow.com/questions/32499936/cancancan-nested-resources-and-abilities-in-database

  14. Raghuveer says:

    Sir even i am creating same application ,i have followed your steps . In web page i am not getting any permission names or checkboxes. Any help would be appreciated.

  15. Raghuveer says:

    I have done same application as you said ,i am using rails 4.2 and sqlite3.in my application i am not able load permissions of all the model

  16. Vishal says:

    Hello sir,
    It is great post. i implemented in my project and it works great. but some
    how i want to know is subject class takes model name ?
    If yes how can i restricts that controller methods which dont have model ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s