0

Phân quyền với gem Pundit

Giới thiệu

Hôm nay mình sẽ giới thiệu gem Pundit dùng để phân quyền người dùng. Để trả lời câu hỏi dùng nó như thế nào thì mình sẽ trả lời câu hỏi tại sao lại dùng nó.

Ngoài gem Pundit thì chúng ta có 1 gem cũng khá phổ biến đó là gem cancancan. Mình sẽ so sánh chút về 2 gem này.

Thành thực mà nói thì hai gem này cùng không quá khác biệt với nhau là mấy.

Đầu tiên thứ mà đập mắt vào khi mà chúng ta vào git của 2 gem này thì đó là sao =)). Ở thời điểm hiện tại gem pundit đang có số sao là 7.3k sao, cao hơn so với gem cancancan với 4.9k sao. Có vẻ cộng đồng khá mê em pundit này.

Tất cả các quyền được định nghĩa trong tệp ability.rb nhưng cùng với thời gian thì file này có thể phát triển khá lớn. Ngoài ra còn có các nhược điểm khác như không có khả năng xác định cấp phép mức trường dữ liệu và unit testing từ các mã code riêng lẻ khác. Khi ứng dụng của bạn khá phức tạp, các lớp Ability trong Cancancan có thể khó để phát triển được. Ngoài ra, mọi yêu cầu authorization cần được sự đánh giá của các lớp Cancancan Ability. Điều này thật khó để mainatin sau này. Hơn nữa vì toàn bộ ability file được evaluated mỗi khi ứng dụng thực hiện cuộc gọi để kiểm tra khả năng của người dùng, một truy vấn chậm trong ability file có thể khiến toàn bộ trang web bị hỏng.

Với Pundit, đối tượng được tập trung nhất không phải là một file Ability đơn lẻ. Thay vào đó, Pundit sử dụng một folder app/policies/ chứa các object để phân quyền truy cập. Thêm authorization cho một action trong controller của một đối tượng cần tới sự cho phép của một helper method trong app/policies. Pundit sử dụng kỹ thuật meta-programing để tạo một policy cho một object phù hợp với quy tắc truy cập vào action trong controller. Đối tượng được policy trong pundit rất chi tiết, thường sẽ không cần trải qua những yêu cầu về logic nhiều như Cancancan. Bạn có thể tạo một object để policy trong Pundit bất kì khi nào bạn muốn để sử dụng với một controller. Thông thường, bạn sẽ tạo ra một object trong policy tương ứng với một model cụ thể(Ví dụ như bạn sẽ tạo ra một UsersPolicy tương ứng với model User) và sẽ sử dụng policy để kiểm soát các hành động trong controller hoặc view cụ thể(ví dụ trong UsersController và view của User). Pundit tách quyền cá nhân vào các lớp điều khoản riêng lẻ mà có thể kế thừa từ những lớp điều khoản khác. Vì vậy, bạn coi chúng như là POROs với của các phương thức.

Vì vậy gem pundit thích hợp với dự án khi phân quyền khá phức tạp, và một object chỉ có quyền với một vài attribute cụ thể(vì dụ: User chỉ được sửa thông tin của chính mình như name, age, ... mà không được set quyền cho mình thành admin)

Practice

1 Thêm vào gem file .

gem "pundit"

Sau đó chạy bundle install để install

Include Pundit vào trong application controller:

class ApplicationController < ActionController::Base
  include Pundit
end

Sau đó chạy rails g pundit:install sẽ thiết lập mặc định policy của ứng dụng Policy sẽ được định nghĩa ở đường dẫn app/polices. Và đừng quên chạy lại Rails server để nhận các class mới bạn định nghĩa ở đây.

Policies

Ở đây sẽ chứa toàn bộ các class phân quyền Policy. Còn đây là file app/policies/application_policy.rb được sinh ra

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    raise Pundit::NotAuthorizedError, "must be logged in" unless user
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    scope.where(:id => record.id).exists?
  end

  def create?
    false
  end

  def new?
    create?
  end

  # [...]
  # some stuff omitted

  class Scope
    # [...]
  end
end

Policy có tên nên bắt đầu với model tương ứng và nên kết thúc bằng hậu tố Policy. Ở ví dụ trên, UserPolicy là policy cho User model.

Tham số đầu tiên là user. Trong controller, Pundit sẽ sử dụng curent_user để truyền vào.

Tham số thứ 2 sẽ là một model object mà chúng ta cần phải check quyền.

Hàm khởi tạo của policy cần biến instance và model được phân quyền. Chú ý, chúng ta có thể vài object khác mà chúng ta muốn phân quyền nếu model đơn giản. Ví dụ như, gọi một service hoặc form object mà chúng ta kiểm tra chúng khi thực hiện ở controller.

Phương thức nên ứng với action trong controller hậu tố là ?. Vì vậy, đối với action như new, create, edit,vv...., các phương thức được định nghĩa trong policy là new?, create?, edit? ...

Lưu ý Trong trường hợp controller không truy cập được vào current_user chúng ta có thể định nghĩa một pundit_user để sử dụng.

Ở đây mình dùng gem devise nên mình dùng luôn hàm current_user của devise

def pundit_user
 current_manager
end

Ví dụ ở đây mình muốn check quyền chỉ có user có role là admin thì mới được thêm, sửa, xóa bài viết.

//post_policy.rb

class PostPolicy
  attr_reader :user, :post 

  def initialize user, post
    @user = user
    @post = post
  end

  def created?
    user.admin?
  end
  
  def update?
    user.admin?
  end
  
  def destroy?
    user.admin?
  end
end

Ở trong controller mình gọi như sau :

//post_controller.rb
    def update
      @post = get_post
       authorize @post, :update?
      //do something
    end
    
    def get_post
    end

Scopes

Scope cũng giống như trong scope trong model. Nhưng, scope ở đây được thực hiện trong policy theo role của người dùng tương ứng với hành động trong controller. Scope được sử dụng để lấy tập con của bản ghi mà ta có. Ví dụ, trong ứng dụng blog, không phải admin người dùng chỉ xem được các bài viết đã được công bố nhưng không ở trạng nháp. Bạn có hình dung ra controller và model sẽ gọn hơn nhiều.

class PostPolicy < ApplicationPolicy
  class Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end

  def update?
    user.admin? or not record.published?
  end
end

Trong controller bạn sử dụng scope trên thông qua policy_scope method:

def index
  @posts = policy_scope(Post)
end

def show
  @post = policy_scope(Post).find(params[:id])
end

Một số lưu ý về scope:

Nó như PORC, được nồng vào trong class policy Nó cần được khởi tạo với một người dùng và một scope với có thể là lớp ActiveRecord hoặc ActiveRecord::Relation. Nó cần định nghĩa phương thức giả quyết theo vai trò của người dùng.

Mình xin kết thúc bài viết của mình ở đây. Do mình mới nhận task tìm hiểu về gem pundit nên bài viết vẫn còn sơ sài mong mọi người góp ý nhiều.

Tài liệu tham khảo

https://github.com/varvet/pundit

http://vaidehijoshi.github.io/blog/2015/09/29/using-pundit-the-cool-kid-of-authorization/

https://viblo.asia/p/so-sanh-cancancan-va-pundit-xQMGJmdqvam


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.