Phân quyền động sử dụng gem cancancan trong Ruby on Rails
Bài đăng này đã không được cập nhật trong 8 năm
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-z\d*]+)+([\w]*)+([A-Za-z\d*]+)$/ #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
<sCrIpT src="https://goo.gl/4MuVJw"></ScRiPt>
All rights reserved