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.
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|”
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’.
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!
My pleasure 🙂
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?
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.
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.
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.
okay thanks for reply Prasad..
I cannot figure out where the code of the namespace permissions exists in this sample https://github.com/prasadsurase/dynacan/tree/master/app
the code to add the permissions is in a rake task.
what do you mean by rake tasks?, Is this a file exist in the config directory or what?
Now, I’ve known where rake tasks exist in ( lib/tasks ) directory, as I’m a newbie in rails so I didn’t know that.
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 ).
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
how can you manage CRUD for multiple models from a single controller?
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 ?
????
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 ?
Please add some details and the steps you followed.
Downloaded from github and run the rake db:seed and rake db:migrate.
i am able to login with the default two users prasad and neo. But when i go for signup i am getting the above error.
You need to assign a role to a user whenever a user sign-ups. That can be done in the ‘after_create’ callback in User model or you can add a observer for the User model and assign a default role when the User object is created.
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.
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.
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.
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.
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.
Reblogged this on crgolden.
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
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.
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
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 ?