022.Phân quyền Rails với Pundit.


Cũng là một gem dùng để phân quyền cho Rails, sinh sau đẻ muộn hơn Cancancan. Nhưng hiện tại được Pundit có nhiều github hơn.
1. Setup bài toán đơn giản
Bài toán như sau, chúng ta có 2 loại user là writer, editor.
- Writer có thể create, edit, update và delete các bài post của chính mình. Ngoài ra thì còn có thể xem các bài post của các writer khác.
- Editor thì có thể edit, update, view và delete bất cứ bài post nào khi chưa publish, nhưng không thể tự tạo bài post mới.
# add role cho bảng users
class AddRoleToUser < ActiveRecord::Migration[7.0]
def change
add_column :users, :role, :integer, default: 0
end
end
# add role vào model
class User < ApplicationRecord
# previous code ....
# ...........
enum role: %i[writer editor]
after_initialize :set_default_role, if: :new_record?
# set default role to user if not set
def set_default_role
self.role ||= :writer
end
end
2. Set quyền cho user
gem 'pundit'
bundle install
bundle exec rails g pundit:install #lệnh này sẽ tạo ra file app/policies/application_policy.rb
Bước tiếp theo là thêm module Pundit::Authorization vào trong application_controller.rb
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit::Authorization
end
Và giờ tới phần hay nhất, chúng ta sẽ tạo một file policy cho mỗi model tài nguyên tương ứng, ở đây là post.
bundle exec rails g pundit:policy post
Các bạn sẽ nhận được một file như thế này.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
# NOTE: Be explicit about which records you allow access to!
# def resolve
# scope.all
# end
end
end
Với bài toán ban đầu, chúng ta sẽ set quyền sơ bộ cho writer như sau.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
...
def create?
@user.writer? # a writer is able to create a post
end
def edit?
@user.writer? # a writer is able to edit a post
end
def update?
@user.writer? # a writer can update a post
end
def delete?
@user.writer? # a writer can delete a post
end
end
Cũng giống như Cancancan, policy này sẽ chưa hoạt động nếu chưa được gọi trong controller.
# app/controllers/post_controller.rb
class PostsController < ApplicationController
# ....
def create
@post = current_user.posts.new(post_params)
authorize @post
respond_to do |format|
if @post.save
format.html { redirect_to post_url(@post), notice: 'Post was successfully created.' }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
def publish
authorize @post, :update?
end
end
Nếu mọi chuyện suôn sẻ thì nếu đăng nhập với tài khoản writer lúc này, các bạn có thể tạo post mới. Còn nếu đăng nhập với tài khoản editor thì sẽ gặp lỗi dưới đây.
3. Scope
Trong bài toán ban đầu, writter có toàn quyền với post của mình, còn những quyền của editor thì chỉ giới hạn với những post chưa publish mà thôi. Đây là lúc áp dụng scope của Pundit, nó cũng giống với với scope của ActiveRecord.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
class Scope < Scope
def resolve
if user.editor?
# an editor can only access posts in "draft" status
scope.where(published: false)
else
# can access a post if they are the author
scope.where(user: user)
end
end
end
def show?
@user.writer? || @user.editor?
end
def create?
@user.writer?
end
def edit?
@user.writer? || @user.editor?
end
def update?
@user.writer? || @user.editor?
end
def delete?
@user.writer?
end
end
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
def index
@posts = policy_scope(Post)
end
# GET /posts/1 or /posts/1.json
def show
@post = policy_scope(Post).find(params[:id])
end
end
Vẫn còn một điểm chưa được giải quyết trong bài toán ban đầu, đó là writer toàn quyền với các post của mình, và view được các post của người khác. Mình chưa biết làm sao để có được nhiều hơn một scope cho một user với một model. Cái này đành chờ đội phát triển người ta làm vậy, xem thêm tại đây
4. Customize error message
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Pundit::Authorization
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
end
5. Kiểm tra quyền của user
<% if policy(post).destroy? %>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<% end %>
6. Using Pundit with Rails’ Strong Parameters
Đội phát triển của Pundit xem chừng cũng khá có tâm nghĩ cho người dùng nên mới đẻ ra cái tính năng này, đó là khả năng phân quyền tới cấp độ thuộc tính của model. Và họ làm việc đó qua params. Giả như trường hợp bạn muốn editor có thể thay đổi các trường title, body và summary còn writer thì không thì các bạn có thể làm như sau:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
# ...
def permitted_attributes
if user.editor?
[:title, :body, :summary]
else
[:title, :body]
end
end
end
Và gọi ra ở controller như sau:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
# ...
private
def post_params
params.require(:post).permit(policy(@post).permitted_attributes)
end
end
Nghĩ tới cảnh phải define post_params, update_params … trong từng controller, thì đội phát triển có cho phép chúng ta khai báo params trong policy.
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def permitted_attributes_for_create
[:title, :body]
end
def permitted_attributes_for_edit
[:body]
end
end
7. Customize error message
Cũng giống như Cancancan, hàm authorize của Pundit sẽ mặc định nhận current_user, và để tùy biến lại, các bạn có thể dùng hàm pundit_user trong controller.
def pundit_user
User.find_by_other_means
end
Hoặc là rescue sẵn cho trường hợp không tìm thấy current_user.
class ApplicationPolicy
def initialize(user, record)
raise Pundit::NotAuthorizedError, "must be logged in" unless user
@user = user
@record = record
end
end
8. Tổng kết
Có thể thấy Pundit có những tính năng tương tự với Cancancan, ngoài ra thì việc tổ chức file có chút ưu điểm hơn khi Cancancan mang mọi thứ vào trong một file Ability, còn Pundit thì tách thành nhiều file Policy để dễ quản lý, cũng như thuận tiện trong trường hợp logic trở nên phức tạp.
Bài viết có tham khảo từ Appsignal.
Comments