Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Thành thạo User Permissions trong Rails: Hướng dẫn toàn diện

Yêu cầu phổ biến của ứng dụng web là khả năng gán roles và permissions cụ thể.

Nhiều loại ứng dụng web phân biệt admin và user thường trong cung cấp truy cập hạn chế. Thường thực hiện bằng boolean đơn giản xác định user có phải admin. Tuy nhiên, roles và permissions có thể phức tạp hơn nhiều.

Giá trị ứng dụng nằm ở việc hạn chế truy cập dữ liệu và hành động. Chắc chắn không muốn làm sai. Bài này giải thích cách implement roles và permissions trong ứng dụng Ruby on Rails cơ bản.

Có cần gem quản lý permissions?

Không, không cần gem, đặc biệt nếu app nhỏ và muốn tránh thêm dependency. Tuy nhiên, nếu tìm alternative, đây là các gem phổ biến xử lý roles và permissions:

  • Devise: Gem authentication và roles management, giải pháp phức tạp và robust. Với 21.7k stars GitHub, repo phổ biến nhất bài này, nhưng làm nhiều hơn roles management. Được biết như authentication solution, chỉ áp dụng nếu cần library rất robust.

  • Pundit: Gem dùng simple Ruby objects, có lẽ policy gem đơn giản nhất chúng ta cover. Dễ dùng, authorization tối thiểu, giống pure Ruby. Với 7.3k stars GitHub, hiện là policy gem phổ biến nhất.

  • CanCan: Authorization library hạn chế resource user được truy cập. Tuy nhiên, CanCan bị abandon nhiều năm và chỉ chạy Rails 3 và trước.

  • CanCanCan: Authorization library khác cho Ruby và Rails. Là alternative cho CanCan và hiện được maintain. Với 4.9k stars GitHub, ít phổ biến nhất, nhưng chạy tốt và maintain tốt.

Tất cả gem này tuyệt vời, nhưng không quá khó tự build permissions bằng plain Ruby. Tôi sẽ chỉ cách quản lý permissions không gem, dùng strategy gọi là policy object pattern.

Policy Object Pattern

Policy Object là design pattern xử lý permissions và roles. Dùng mỗi khi phải check xem gì hoặc ai được phép thực hiện hành động. Nó encapsulates business rules phức tạp và dễ replace bằng policy objects khác với rules khác. Tất cả external dependencies được inject vào policy object, encapsulate logic kiểm tra permission, kết quả controller và model sạch. Gem như Pundit, Cancan, Cancancan implement pattern này.

Pure policy object rules

  • Return phải là boolean value
  • Logic phải đơn giản
  • Bên trong method, chỉ gọi methods trên objects được truyền vào

Triển khai

Bắt đầu với naming convention; filename có suffix _policy và class và policy ở cuối. Trong method, tên luôn kết thúc bằng ? (ví dụ: UsersPolicy#allowed?).

Ví dụ code:

class UsersPolicy
  def initialize(user)
    @user = user
  end

  def allowed?
    admin? || editor?
  end

  def editor?
    @user.where(editor: true)
  end

  def admin?
    @user.where(admin: true)
  end
end

Trong scenario nào nên dùng?

Khi app có nhiều loại restricted access và restricted actions. Ví dụ, posts có thể tạo với:

  • ít nhất một tag,
  • hạn chế chỉ admin và editor tạo, và
  • yêu cầu editor cần verified.

Ví dụ controller không policy object:

class PostsController < ApplicationController
  def create
    if @post.tag_ids.size > 0 && (current_user.role == 'admin' || (current_user.role == 'editor' && current_user.verified_email))
      # create
    end
  end
end

Vì điều kiện check dài, xấu, khó đọc, nên áp dụng policy object pattern.

Bắt đầu tạo PostsCreationPolicy.

class PostsCreationPolicy
  attr_reader :user, :post

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

  def self.create?(user, post)
    new(user, post).create?
  end

  def create?
    with_tags? && author_is_allowed?
  end

  private

  def with_tags?
    post.tag_ids.size > 0
  end

  def author_is_allowed?
    is_admin? || editor_is_verified?
  end

  def is_admin?
    user.role == 'admin'
  end

  def editor_is_verified?
    user.role == 'editor' && user.verified_email
  end
end

Controller với policy object trông như:

class PostsController < ApplicationController
  def create
    if PostsCreationPolicy.create?(current_user, @post)
      # create
    end
  end
end

Cách dùng policy objects trong Rails

Tạo thư mục policy trong app /policies và đặt tất cả policy classes đó. Khi cần gọi controller, làm trực tiếp trong action hoặc dùng before_action:

class PostsController < ApplicationController
  before_action :authorized?, only: [:edit, :create, :update, :destroy]

  def authorized?
    unless ::PostsCreationPolicy.create?(current_user, @post)
      render :file => "public/404.html", :status => :unauthorized
    end
  end
end

Cách test policy objects

Dễ dàng test behavior trong controller:

require 'rails_helper'
RSpec.describe "/posts", type: :request do
  describe "when user is not allowed" do
    let(:user_not_allowed) { create(:user, admin: false, editor: false) }
    let(:tag) { create(:tag) }
    let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }
    before do
      sign_in user_not_allowed
    end
    describe "GET /index" do
      it "return code 401" do
        diet = Post.create! valid_attributes
        get edit_post_url(post)
        expect(response).to have_http_status(401)
      end
    end
  end
end

Test policy cũng đơn giản; có nhiều method nhỏ chỉ một trách nhiệm.

require 'rails_helper'
RSpec.describe PostsCreationPolicy do
  describe "when user is not allowed" do
    let(:user) { create(:user, editor: false, admin: false) }
    let(:user_editor) { create(:user, editor: true, email: verified) }
    let(:tag) { create(:tag) }
    let(:post) { create(:post, tag_id: tag.id) }
    describe ".create?" do
      context "when user is allowed" do
        it "creates a new post" do
          expect(described_class.create?(user_editor, post)).to eq(true)
        end
      end
      context "when user is not allowed" do
        it "does not create a new post" do
          expected(described_class.create?(user, post)).to eq(false)
        end
      end
    end
    # ...more test cases
  end
end

Test xem object có được phép tạo trong mọi scenario.

Kết luận

Khái niệm policy pattern nhỏ nhưng tạo kết quả lớn. Cân nhắc áp dụng policy object mỗi khi xử lý permissions đơn giản hoặc phức tạp. Khi test với RSpec, không cần database records; policies là pure Ruby objects, test đơn giản và nhanh.