Created
July 18, 2015 11:03
-
-
Save cmthakur/5f1db017d6d7ab63c5a0 to your computer and use it in GitHub Desktop.
RBAC with cancan
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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