12/08/2018, 13:22

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

0