Skip to content

Instantly share code, notes, and snippets.

@cmthakur
Created July 18, 2015 11:03
Show Gist options
  • Select an option

  • Save cmthakur/5f1db017d6d7ab63c5a0 to your computer and use it in GitHub Desktop.

Select an option

Save cmthakur/5f1db017d6d7ab63c5a0 to your computer and use it in GitHub Desktop.
RBAC with cancan
# 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
# 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
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
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment