Phân quyền trong Rails sử dụng Pundit
I. Giới thiệu Với mỗi ứng dụng web bất kì, phân quyền cho người dùng là việc làm không thể thiếu. Đối với các ứng dụng sử dụng ruby on rails, chúng ta có một số thư viện hỗ trợ việc phân quyền như cancancan, pundit, authorite... Những thư viện này sẽ giúp chúng ta tách hoàn toàn phần logic phân ...
I. Giới thiệu
Với mỗi ứng dụng web bất kì, phân quyền cho người dùng là việc làm không thể thiếu. Đối với các ứng dụng sử dụng ruby on rails, chúng ta có một số thư viện hỗ trợ việc phân quyền như cancancan, pundit, authorite... Những thư viện này sẽ giúp chúng ta tách hoàn toàn phần logic phân quyền ra khỏi controller hay model. Một trong những gem phân quyền được phát triển sớm và phổ biến nhất là Cancancan hay trước đây là Cancan. Đối với Cancancan toàn bộ những logic phân quyền sẽ được cho vào class có tên Ability. Tuy nhiên việc tất cả các logic phân quyền tập trung chỉ trong một class Ability sẽ làm cho file này phình to lên nhanh chóng. Một điểm không thực sự tốt nữa ở Cancancan là role của user sẽ cần được đánh giá qua từng request từ đó làm chậm đáng kể thời gian phản hồi. Gần đây, trong dự án mới mình có dịp tìm hiểu và dùng thử một gem phân quyền khác là Pundit và nhận ra nó có khá nhiều ưu điểm so với Cancancan. Hôm nay mình xin giới thiệu với các bạn về Pundit.
II. Sự khác biệt của Pundit với Cancancan
Pundit ra đời sau tuy nhiên đã phát triển nhanh chóng và có khá nhiều những ưu điểm so với Cancancan. Đầu tiên các bạn có thể xem so sánh tổng quan giữa Pundit và Cancancan tại đây.
Pundit sử gọn nhẹ hơn nhiều so với Cancancan.
Pundit sử dụng cú pháp DSL (domain specific language) đơn giản hơn so với Cancancan giúp nó thực sự dễ tìm hiểu và áp dụng.
Ngoài ra, như đã nói ở trên, đối với Cancancan toàn bộ logic về phân quyền sẽ được lưu trong file app/models/ability.rb. Còn với Pundit mỗi model bạn sẽ có một class ruby tương ứng đảm nhiệm việc phân quyền của người dùng cho model ấy. Và trong mỗi class policy ấy sẽ có các hàm tương ứng dành cho từng action của controller. Với cá nhân mình cách chia nhỏ như vậy của Pundit giúp chúng ta dễ lắm bắt và kiểm soát hơn nhiều.
III. Cài đặt và sử dụng Pundit
Chúng ta sẽ bắt đầu bằng một ứng dụng đơn giản gồm 2 model chính là User và Post như hình:
1. Cài đặt Pundit
Thêm vào Gemfile
gem "pundit"
Include Pundit trong application_controller
class ApplicationController < ActionController::Base include Pundit protect_from_forgery end
Tiếp theo chúng ta sẽ tạo một application policy và các policy cho từng model sau đó sẽ kế thừa từ application policy này
rails g pundit:install
Sau khi chạy câu lệnh generate chúng ta sẽ có một thư mục app/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
Đến đây ta có thể thấy:
- Các class Policy được đặt tên dưới dạng ModelNamePolicy.
- 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.
- Policy class đã implements các phương thức update?, new?... Các phương thức này sẽ tương ứng với các action của controller
2. Phân quyền cho model
Giờ là lúc chúng ta bắt đầu phân quyền cho model Post. Giả sử chỉ có user với role là admin được quyền xoá post, ta sẽ có: policies/post_policy.rb
class PostPolicy < ApplicationPolicy def destroy? user.admin? end end
Tương tự với các method khác của class policy, method destroy? ở đây cũng trả về true hoặc false để xác định quyền của user với model.
Và với định nghĩa như trên, ở controller ta sẽ check quyền bằng phương thức `
authorize
post_controller.rb`
def update @post = Post.find(params[:id]) authorize @post if @post.update(post_params) redirect_to @post else render :edit end end
Nếu muốn check quyền khi ở một action không giống với policy action thì ta có thể truyền trực tiếp tên của action policy cần được check:
def publish authorize @post, :update? end
Ở đây hàm authorized có thể diễn đạt đầy đủ thành:
raise "not authorized" unless PostPolicy.new(current_user, @post).update?
Khi user không có quyền thực hiện action, chúng ta cần xử lí exception của Pundit trong application_controller application_controller.rb
[...] rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:warning] = "You are not authorized to perform this action." redirect_to(request.referrer || root_path) end [...]
config/locales/en.yml
en: pundit: default: 'You cannot perform this action.'
Sau khi sử lí ở controller, ta cần kiểm tra cả ở view để ẩn hiện các link phù hợp với từng action. views/posts/index.html.erb
<% if policy(post).destroy? %> <td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td> <% end %>
3. Sử dụng Pundit với scope
Đôi khi bạn cần hiển thị một list các record tương ứng với một user nhất định nào đấy. Và Pundit cung cấp tính năng scope cho phép bạn thực hiện việc này dễ dàng.
class PostPolicy < ApplicationPolicy class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve if user.admin? scope.all else scope.where(published: true) end end end end
Giờ bạn có thể dùng class này ở controller thông qua method policy_scope post_controller.rb
def index @posts = policy_scope(Post) end
views/posts/index.html.erb
<% policy_scope(@user.posts).each do |post| %> <p><%= link_to post.title, post_path(post) %></p> <% end %>
III. Kết luận
Trên đây mình đã giới thiệu với các bạn về gem Pundit, một sự lựa chọn đáng cân nhắc nếu bạn đang tìm kiếm giải pháp phân quyền cho ứng dụng Rails của mình.
Nguồn tham khảo
https://github.com/elabs/pundit https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/