30/03/2021, 09:46

Hiểu rõ hơn về gem CanCanCan trong Ruby On Rails- Part 1

CanCanCan là một thư viện phân quyền cho Ruby và Ruby on Rails, nó hạn chế những tài nguyên mà một người dùng nhất định được phép truy cập. Tất cả các quyền có thể được xác định trong một hoặc nhiều tệp khả năng và không bị trùng lặp trên controller, view và query DB, giữ logic phân quyền ở một ...

  • CanCanCan là một thư viện phân quyền cho Ruby và Ruby on Rails, nó hạn chế những tài nguyên mà một người dùng nhất định được phép truy cập.
  • Tất cả các quyền có thể được xác định trong một hoặc nhiều tệp khả năng và không bị trùng lặp trên controller, view và query DB, giữ logic phân quyền ở một nơi để dễ dàng bảo trì và kiểm tra.

Điều gì sẽ sảy nếu bạn hoặc khách hàng muốn thay đổi quyền mà không phải triển khai lại Ứng dụng? Trong trường hợp đó, tốt nhất có thể lưu trữ logic quyền trong cơ sở dữ liệu: rất dễ sử dụng các bản ghi cơ sở dữ liệu khi xác định các khả năng.

    class Ability
      include CanCan::Ability

      def initialize user
        if user.admin?
          can :manage, :all
          cannot :create, Borrowing, user_id: user.id
          cannot :read, Borrowing
        else
          can :create, Borrowing
          can :read, Borrowing, user_id: user.id
          can :read, Borrowing, ["user_id = ?", user.id] do |borrowing|
            borrowing.created_at.year >= 2021
          end
        end
      end
   end

Trong CanCanCan các action được thể hiện như sau:

def eval_cancan_action(action)
  case action.to_s
  when "index", "show", "search"
    cancan_action = "read"
    action_desc = I18n.t :read
  when "create", "new"
    cancan_action = "create"
    action_desc = I18n.t :create
  when "edit", "update"
    cancan_action = "update"
    action_desc = I18n.t :edit
  when "delete", "destroy"
    cancan_action = "delete"
    action_desc = I18n.t :delete
  else
    cancan_action = action.to_s
    action_desc = "Other: " << cancan_action
  end

  return action_desc, cancan_action
end

Điều gì xảy ra nếu bạn muốn xác định quyền của Người dùng không phải là current_user ? Giả sử mình muốn xem liệu một user khác có quyền xem được phiếu mượn hay không
some_user.ability.can? :update, @borrowing

Thêm phương thức ability vào model User:

def ability
  @ability ||= Ability.new(self)
end

Sử sụng delegate để có thể gọi trực tiếp từ User:

class User < ActiveRecord::Base
  delegate :can?, :cannot?, to: :ability
end

Kết quả sẽ được như sau:

User.last.can? :read, Borrowing.first
   (0.7ms) 
  User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` DESC LIMIT 1
  Borrowing Load (1.3ms)  SELECT `borrowings`.* FROM `borrowings` WHERE `borrowings`.`deleted_at` IS NULL ORDER BY `borrowings`.`id` ASC LIMIT 1
=> false

Cuối cùng, nếu bạn muốn xem current_user có quyền truy cập vào object nào không thì tốt nhất bạn nên ghi đè mothod current_ability trong ApplicationController.

def current_ability
  @current_ability ||= current_user.ability
end

Kết quả sẽ được test như sau:

current_ability.can? :create, Borrowing
=> false

Một ability rule sẽ ghi đè một ability rule trước đó. Giả sử Admin có toàn quyền với Borrowing, nhưng không được xoá chúng.

  can :manage, Borrowing
  cannot :destroy, Borrowing

Điều quan trọng là cannot :destroy, Borrowing sau dòng can :manage, Borrowing . Như vậy,cannot :destroy sẽ bị overridden bởi can :manage.

Việc thêm can không ghi đè rule trước đó.

   can :read, Borrowing, user_id: user.id
   can :read, Borrowing do |borrowing|
     borrowing.created_at.year > 2021
   end

can? :read sẽ luôn trả về true nếu user_id = user.id kể cả nếu Borrowing có thời gian tạo nhỏ hơn 2021

Điều gì sẽ xảy ra nếu bạn cần sửa đổi các quyền dựa trên thứ gì đó bên ngoài đối tượng User? Giả sử bạn muốn đưa các địa chỉ IP nhất định vào danh sách đen khỏi việc tạo comment. Địa chỉ IP có thể truy cập thông qua request.remote_ip nhưng class Ability không có quyền truy cập vào địa chỉ này. Cách đơn giản đề sửa đổi những gì mà bạn truyền vào cho đối tượng Ability bằng việc ghi đè phương thức current_ability trong ApplicationController.

class ApplicationController < ActionController::Base
  private

  def current_ability
    @current_ability ||= Ability.new(current_user, request.remote_ip)
  end
end
class Ability
  include CanCan::Ability

  def initialize(user, ip_address=nil)
    can :create, Comment unless BLACKLIST_IPS.include? ip_address
  end
end

Concept này cũng có thể áp dụng cho Session hay Cookie.

Mặc định trong gem CanCanCan là quyền dựa trên user và object được xác định trong load_resource. Nhưng nếu bạn có 1 SearchController và Admin::SearchController, bạn có thể sử dụng một số tiếp cận khác.
Trong trường hợp này, chỉ cần ghi đè lại method current_ability trong ApplicationController để include controller namespace, và tạo một class Ability biết phải làm gì với nó

class ApplicationController < ActionController::Base

  private
  
  def current_ability
    controller_name_segments = params[:controller].split('/')
    controller_name_segments.pop
    controller_namespace = controller_name_segments.join('/').camelize
    @current_ability ||= Ability.new(current_user, controller_namespace)
  end
end
class Ability
  include CanCan::Ability

  def initialize(user, controller_namespace)
    case controller_namespace
      when "Admin"
        can :manage, :all if user.admin?
      else
        # rules for non-admin controllers here
    end
  end
end

Còn một cách khác là sử dụng một Ability class khác trong controller này:

class Admin::BaseController < ActionController::Base
  #...

  private

  def current_ability
    @current_ability ||= AdminAbility.new(current_user)
  end
end

Kết luận

Thông qua bài viết này, mình mong giúp các bạn hiểu hơn về gem CanCanCan.

0