Phân quyền động sử dụng gem cancancan trong Ruby on Rails
Hầu hết chúng ta đã sử dụng gem cancancan để phân quyền. Chúng ta có thể định nghĩa các quyền truy cập tới model khác nhau trong class Ability. class Ability include CanCan : : Ability def initialize user end end Tuy nhiên, khi thay đổi bất kì Ability nào, chúng ta đều ...
Hầu hết chúng ta đã sử dụng gem cancancan để phân quyền. Chúng ta có thể định nghĩa các quyền truy cập tới model khác nhau trong class Ability.
class Ability include CanCan::Ability def initialize user end end
Tuy nhiên, khi thay đổi bất kì Ability nào, chúng ta đều phải thay đổi lại code trong class Ability và chạy lại app để thay đổi có tác dụng. Vậy tại sao không nghĩ tới viêc phân quyền 1 cách tự động? Điều này thật tuyệt vời phải không, chúng ta có thể kiểm soát phân quyền trong hệ thống một cách tự động tới từng người dùng trong hệ thống.
Hãy bắt đầu với 1 ví dụ sau đây.
Cài đặt gem
gem "cancancan"
Chúng ta sẽ thêm các model cần thiết. Tuy nhiên cần phải có 3 model cơ bản User, Role, Permission với quan hệ như sau
# app/models/permission.rb class Permission < ActiveRecord::Base belongs_to :role ATTRIBUTES_PARAMS = [:subject_class, :action] # :subject_class lưu tên model ví dụ như Book, Author ... # :action tên các action trong controller ví dụ như create, update, hoặc destroy end # app/models/role.rb class Role < ActiveRecord::Base has_many :users has_many :permissions ATTRIBUTES_PARAMS = [:name] end # app/models/user.rb class User < ActiveRecord::Base belongs_to :role ATTRIBUTES_PARAMS = [:email, :password, :username] end
Đầu tiên, chúng ta cần phải định nghĩa protected methods trong application controller
# # app/controllers/appication_controller.rb class ApplicationController < ActionController::Base protect_from_forgery with: :exception before_action :authenticate_user! rescue_from CanCan::AccessDenied do |exception| flash[:alert] = "Access denied. You are not authorized to access the requested page." redirect_to root_path end protected # lấy ra tên model từ controller ví dụ: UsersController sẽ lấy ra được là User class << self def permission return name = self.name.gsub("Controller","").singularize.split("::").last.constantize.name rescue nil end end def current_ability @current_ability ||= Ability.new current_user end # lấy ra tất cả các permissions của user def load_permissions @current_permissions = current_user.role.permissions.collect{|p| [p.subject_class, p.action]} end end
Nếu bạn định nghĩa controller cho model khác ví dụ như InvitesController cho model Invitation hoặc controller khai báo trong 1 namespace, bạn có thể override lại method
# app/controller/invites_controller.rb private class << self def permission return "Invitation" end end
hoặc
class << self def permission return "Namescope::Model" end end
Tiếp theo, class Ability cần được khai báo như sau:
# 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 end
Trong tất cả các controllers đều cần phải khai báo các callback ví dụ như
# app/controller/invites_controller.rb class InvitesController < ApplicationController load_and_authorize_resource before_action :load_permissions # call this after load_and_authorize else it gives a cancancan error ... end
Về cơ bản các bước cài đăt đã hoàn hiện. Bây giờ, chúng ta sẽ tạo 1 file rake task để tìm kiếm tất cả các controllers và tạo các quyền cho tất cả các public method trong controller
# app/lib/tasks/create_permissions.rake namespace :db do desc "Loading all models and their related controller methods inpermissions table." task create_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. create_permission controller.permission, "manage", 'manage' #add permission to do CRUD for every model. controller.action_methods.each do |method| if method =~ /^([A-Za-zd*]+)+([w]*)+([A-Za-zd*]+)$/ #add_user, add_user_info, Add_user, add_User name, cancan_action = eval_cancan_action method create_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 when "new", "create" name = "create and update" cancan_action = "create" when "show" name = 'view' cancan_action = "view" when "edit", "update" name = 'create and update' cancan_action = "update" when "delete", "destroy" name = "delete" cancan_action = "destroy" else #in case you do not follow RESTFUL name = action.to_s cancan_action = action.to_s end return name, cancan_action end #check if the permission is present else add a new one. def create_permission model, cancan_action, name permission = Permission.find_by subject_class: model, action: cancan_action Permission.create name: name, subject_class: model, action: cancan_action unless permission end
Trong hầu hết các trường hợp, các roles thường được xác định sẵn nên chúng ta có thể tạo thông qua seed file hoặc 1 rake task. Cần có 1 user với phân quyền cao nhất để có thể chia quyền cho các user khác, "Super Admin".
namespace :db do desc "Create roles, permissions and users" task create_base_data: :environment do puts "Create roles" Role.create! name: "Super Admin" Role.create! name: "Staff" puts "Create a universal permission" Permission.create! subject_class: "all", action: "manage" puts "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") puts "Create a user and assign the super admin role to him" user = User.new username: "New user", email: "foo_bar@gmail.com", password: "foobar", password_confirmation : "foobar" user.role = role user.save! User.create name: "Neo", email: "neo@matrix.com", password: "the_one", password_confirmation: "the_one", role: Role.find_by_name("Staff") end end
Bây giờ chúng ta cần chạy 2 lệnh rake task
rake db:create_base_data rake db:create_permissions
Vậy là chúng ta đã có đầy đủ dữ liệu cho app. Giả sử bạn muốn thực hiện thao tác CRUD trong 2 models, bạn sẽ tạo ra các controller tương ứng
# app/models/part.rb class Part < ActiveRecord::Base has_many :drawings end # app/models/drawing.rb class Drawing < ActiveRecord::Base belongs_to :part end
Nếu như muốn phân quyền trong hệ thống, bạn cần phải tạo 1 controller.
# app/controllers/roles_controller.rb class RolesController < ApplicationController # only user with super admin role can access before_action :is_super_admin? before_action :find_role, only: [:show, :edit, :update] 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 @permissions = @role.permissions end def edit #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.permissions = [] @role.set_permissions params[:permissions] if params[:permissions] redirect_to roles_path if @role.save @permissions = Permission.all.keep_if{|i| ["Part"].include? i.subject_class}.compact render :edit end private def is_super_admin? redirect_to root_path unless current_user.super_admin? end def find_role @role = Role.find params[:id] end end
Bạn cần tạo 1 public function trong model Role để phân quyền 1 trong số các quyền tới các model khác nhau với các quyền cơ bản. Tất nhiên nó cũng sẽ phụ thuộc vào app của bạn.
# 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" self.permissions << case permission.action #if create part permission is assigned then assign create drawing as well when "create" Permission.where subject_class: "Drawing", action: "create" #if update part permission is assigned then assign create and delete drawing as well when "update" Permission.where subject_class: "Drawing", action: ["update", "destroy"] end end end end ...
Ưu điểm của phương pháp này là SuperAdmin có thể phân quyền trên một web Ui. Các quyền được load trong before_action và có thể sử dụng tự động sau khi được cấp phép cho các người dùng khác nhau theo từng role. Bên cạnh đó, SuperAdmin có thể tạo tự động Role and phân các quyền cho role mới
Tuy nhiên, phương pháp này cũng có nhược điểm là nếu các controller và model quá nhiều thì các quyền cũng sẽ nhiều theo tương ứng. Vì thế nên SuperAdmin cần phải hiểu rất rõ về tất cả các methods của tất cả các controller để có thể phân quyền cho user tương ứng.
Bài viết này có tham khảo từ dynamic-roles-and-permissions-using-cancan