Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ User Permissions ใน Rails: คู่มือครบครัน

Common Requirement ของ Web Apps: Assign Specific Roles และ Permissions

หลาย Web Apps แยก Admins และ Regular Users ด้วย Simple Boolean (is_admin?) แต่ Roles/Permissions อาจ Complex มากขึ้น

Value ของ App อยู่ที่ Restrict Access Data/Actions อย่างแน่นอน อย่า Mess Up บทความนี้อธิบาย Implement Roles/Permissions ใน Basic Rails App

Do I need a gem? (ต้องใช้ Gem ไหม?)

ไม่จำเป็น หาก App เล็กอยากหลีกเลี่ยง Dependencies แต่ Alternatives มี Gems ยอดนิยม:

  • Devise: Authentication + Roles Management Complex/Robust 21.7k Stars GitHub Most Popular แต่ทำมากกว่า Roles ถ้าต้องการ Robust Library
  • Pundit: Simple Ruby Objects Simplest Policy Gem 7.3k Stars Most Popular Policy Gem Minimal Authorization ใกล้ Pure Ruby
  • CanCan: Authorization Library Restrict Resources แต่ Abandoned Years ใช้ได้ Rails 3 เท่านั้น
  • CanCanCan: Alternative CanCan Maintained 4.9k Stars Least Popular แต่ Work Well Maintained

Gems ทั้งหมดดี แต่ Build Permissions เองด้วย Plain Ruby ไมยาก ใช้ Strategy Policy Object Pattern

Policy Object Pattern

Design Pattern จัดการ Permissions/Roles ใช้ทุกครั้งที่ Check Allowed Action Encapsulate Complex Business Rules Replaceable ด้วย Policy Objects อื่นๆ Rules ต่าง External Dependencies Injected -> Clean Controller/Model Gems (Pundit, CanCan, CanCanCan) Implement Pattern นี้

Pure Policy Object Rules

  • Return = Boolean
  • Logic = Simple
  • Inside Method เรียก Methods บน Passed Objects เท่านั้น

Implementation (การนำไปใช้)

Naming Convention: Filename Suffix _policy Class Name Suffix Policy Method Names End with ? (e.g. UsersPolicy#allowed?)

Example:

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

When to Use? (สถานการณ์ไหนควรใช้)

App มี Multiple Restricted Access/Actions เช่น Posts Create ต้อง:

  • At least 1 Tag
  • Only Admins/Editors Create
  • Editors ต้อง Verified

Controller โดยไม่มี Policy Object (Long, Ugly, Unreadable):

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

Apply Policy Object Pattern สร้าง 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 ด้วย Policy Object (Clean):

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

How to Use in Rails

สร้าง Directory app/policies ใส่ Policy Classes เรียกใน Controller โดยตรง หรือ 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

How to Test Policy Objects

Test Controller Behavior ง่าย:

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
        post = Post.create! valid_attributes
        get edit_post_url(post)
        expect(response).to have_http_status(401)
      end
    end
  end
end

Test Policy เองง่าย เพราะ Small Methods Single Responsibility:

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
          expect(described_class.create?(user, post)).to eq(false)
        end
      end
    end
  end
end

Test ว่า Object Allowed Create ในทุก Scenario

Conclusion (สรุป)

Policy Pattern Concept เล็กแต่ผลใหญ่ Apply Policy Object ทุกครั้งที่ Deal Permissions (Simple/Complex) Testing RSpec ไม่ต้อง Database Records Policies = Pure Ruby Objects Testing Simple/Fast