Roles permission – Chuyển từ Cancancan sang Pundit

thời gian này app của chúng tôi đã chuyển từ CanCanCan thành Pundit. CanCanCan là một gem tuyệt vời nhưng chúng tôi đã phát huy nó thêm nữa. Đây là những bài học khác nhau.

Thứ nhất, phải thừa nhận rằng CanCanCan rất dễ để bắt đầu và có sự trang bị tuyệt vời với RailsAdmin, lập trình viênise và các gem khác. Tất cả các quyền được được hiểu trong tệp ability.rb nhưng cùng với thời gian thì file này có thể phát huy khá lớn. thêm nữa còn có các nhược điểm khác như không có kỹ xảo đánh giá cấp phép mức trường data và unit testing từ các mã code riêng lẻ khác.

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.

Grouping policies

Thông thường bạn có nhiều model mà cần phải chia sẻ cùng quyền. Vì vậy, có thể bạn không thích tạo ra policy files cho đã có lần models và lặp lại code. Kể từ policy file của bạn chỉ là lớp Ruby bạn có thể làm điều này:

# app/policies/application_policy.rb class ApplicationPolicy   # define common permissions here end class UserPolicy < ApplicationPolicy   # customize permissions for various methods index?, show?, etc   # call super if needed end class CommonPolicy < ApplicationPolicy   ... end 

Model của bạn có thể trông sẽ như thế này. Chúng tôi đang dùng Mongoid nhưng design giống như sẽ làm việc với ActiveRecord.

# app/models/user.rb class User   belongs_to :client   # will automatically use UserPolicy end class Client   has_many :accounts   has_many :users   def self.policy_class     CommonPolicy # manuall specify policy   end end class Account   belongs_to :client   def self.policy_class     CommonPolicy   end end class Company   belongs_to :client   def self.policy_class     CommonPolicy   end end 

Điều này có thể là một tình thế tương đồng với Single Table Inhertiance. Thông thường các điều khoản cho các mô hình khác nhau có gốc gác kiểu models cơ bản là giống nhau, do đó bạn có thể chia sẻ các policy.

Hoặc bạn có thể tạo ra các chính sách riêng cho đã có lần Client, AccountCompany và tiếp theo bạn sẽ không cần phải hành động self.policy_class. Bạn cũng có thể chỉ định điều khoản cụ thể hơn cho Client, Account Company models nếu rất cần.

class ClientPolicy < CommonPolicy   ... end class AccountPolicy < CommonPolicy   ... end class CompanyPolicy < CommonPolicy   ... end 

Mapping roles to permissions

Để cho mọi thứ dễ làm ta có thể làm một bảng UserClient và belongs_to client và user

class User   has_one :user_client end class Client   has_many :user_clients end class UserClient   belongs_to :client   belongs_to :user   field :roles, type: Array   extend Enumerize   enumerize :roles, in: [:admin, :readonly_admin, :account_admin, :company_admin],   multiple: true   end class UserClientPolicy < ApplicationPolicy   ... end 

admin có thể điều chỉnh client và thực hiện các quyền CRUD trên bản ghi client con.readonly_admin chỉ có thể xem tất cả bản ghi, account_admin có thể làm các thao tác CRUD trên account và company_admin thể làm giống như cho các bản ghi company. Đối với điều này, chúng tôi cần tạo policy riêng cho Client, Account and Company models.

thêm nữa hệ thống còn có vai trò mở rộng (cho người sử dụng nội bộ) được đánh giá trực tiếp trên User model. Chỉ người sử dụng nội bộ có thể tạo / hủy tạo ra client mới nhưng Client Admins có thể đổi khác các thuộc tính của Client.

class User   extend Enumerize   enumerize :roles, in: [:sysadmin, :acnt_mngr], multiple: true   end 

Điều này sẽ cung ứng cho người sử dụng nội bộ vào web vào tất cả bản ghi.

class ApplicationPolicy   def index?     return true if @user.roles.include? ['sysadmin', 'acnt_mngr']   end   def show?     index?   end   def update?     index?   end   def edit?     index?   end   def create?     index?   end   def new?     index?   end   def destroy?     # must have higher level permissions       return true if @user.roles.include? ['sysadmin']   end end 

Vì vậy, công việc này rất tốt cho việc cấp phép một app lớn nhưng client phải được đặc tả một cách rõ ràng hơn. thêm nữa khi chúng tôi đang trong show, edit, update hoặc destroy ta có thể lấy được client từ các bản ghi. Trong số đó ta có nhiều bản ghi và trong new / create bản ghi chưa hề tồn tại, vì vậy chúng tôi cần get client từ user.

class User   def get_client_id     user_client.client_id   end end class ApplicationPolicy   def get_client_id     # or we could just always get client from user     return @record.client_id if @record.try(:client_id)     return @user.get_client_id   end end 

Điều này sẽ cho vào web vào bản ghi Client với quyền chỉ đọc thông qua chỉ mục và hiển thị cho admin và readonly_admin và thêm quyền vào web edit / update cho các roles khác.

class ClientPolicy   def index?     return true if @user.user_clients.where(client: get_client_id)     .in(roles: ['admin', 'readonly_admin']).count > 0     super   end   def show?     index?   end   def edit?     return true if @user.user_clients.where(client: get_client_id)     .in(roles: ['admin']).count > 0           super   end   def update?     edit?   end   #  new?, create? and destroy? are not set so it uses ApplicationPolicy end 

Kiểm tra cho @user.user_clients.where(client: @record.client).in(roles: …) hiện tại đang không DRY vì vậy ta có thể trích xuất nó vào lớp khác nhau.

# app/services/role_check.rb class RoleCheck   def initialize user:, client:, roles: nil     @user = user     @client = client     @roles = roles   end   def perform     return true if @user.roles.include? :sysadmin     roles2 = [:admin, @roles].flatten     return true if @user.user_clients.in(client_id: @client)       .in(roles: roles2).count > 0   end end # class ClientPolicy   def index?     RoleCheck.new(user: user, client: get_client_id,       roles: [:client_admin, :readonly_admin]).perform   end end 

Quyền cho Account và Company hơi khác một chút

class AccountPolicy   def index?     RoleCheck.new(user: user, client: get_client_id,       roles: [:account_admin, :readonly_admin]).perform     super   end   def show?     index?   end   def edit?     RoleCheck.new(user: user, client: get_client_id,       roles: [:account_admin]).perform     super   end   def update?     edit?     # same checks for new?, create? and destroy?   end end 
class CompanyPolicy   # similar checks using 'company_admin' role end 

Bạn cũng có thể dùng gem Rolify để trỏ users tới roles nhưng ta đã có model UserClient vì những lý do khác vì vậy ta thừa hưởng điều đó.

Beyond RESTful actions

Bạn bắt đầu với :index?, :show? … nhưng tiếp nữa ta cần được hiểu thêm các quyền mà ta muốn điểu chỉnh. ta có thể nói user trở thành admin từ việc activate? một account

class AccountPolicy   def activate?     # no need to pass admin role as RoleCheck automatically includes it     RoleCheck.new(user: user, client: @record.client).perform   end end 

Những loại hành động tùy chỉnh thường sẽ được đặc tả cụ thể cho một model nhưng nếu chúng là chung cho nhiều model bạn có thể đẩy chúng vào lớp policy thấp và kế thừa từ nó trong đặc tả policy model.

Để kiểm tra các điều khoản tùy chỉnh bạn có thể tạo ra một hành động non-RESTful trong AccountsController của bạn.

class AccountsController < ApplicationController   def activate     authorize @account     @account.update(status: 'active')   end end # or to stick with traditional REST actions you create a separate controller class Accounts::ActivateController < ApplicationController   def update     authorize @account     @account.update(status: 'active')   end end 

tiếp nữa bạn gọi chính xác.

Require authorize in application controller for all actions

Cá nhân tôi thích yêu cầu chính xác cho tất cả các hành động trong controller ngay cả khi tôi đặt def index? true; hoàn thành để cho mọi người sử dụng.

class AccountsController < ApplicationController   after_action except: [:index] { authorize @account }   after_action only:   [:index] { authorize @accounts } end 

Headless policies

Giả sử bạn có trường report_admin cấp quyền người sử dụng chạy các báo cáo khác nhau từ bảng điều khiển.

class DashboardPolicy < Struct.new(:user, :dashboard)   def index?     RoleCheck.new(user: user, client: user.get_client_id,     roles: [:report_admin]).perform     end end 
# somehere in the UI navbar <%= đường dẫn_to('Dashboard', dashboard_index_path) if policy(:dashboard).index? %> | 

Kiểm tra cam kết rằng file policy của bạn chỉ chứa những quyền cơ bản. Khi bạn chạy rails g pundit:policy bảng điều khiển sẽ bao gồm các chỗ cho class Scope < Scope. Nếu không, bạn đó là chuyện của github.

Pundit::NotDefinedError at /dashboard unable to find policy `DashboardPolicy` for `:dashboard` 

Scopes

Internal users có thể xem tất cả các records, client specific users chỉ có thể xem các accountcompany được phân chia cho client đó.

class AccountPolicy < ApplicationPolicy   ...   class Scope < Scope     def resolve       if @user.roles.include? ['sysadmin', 'acnt_mngr']         scope.all       else         scope.in(client_id: @user.get_client_id)       end     end   end end 

Field level permissions

Đôi khi bạn cần phải đánh giá quyền vào web vào trường cụ thể w / in record. Giả sử rằng chỉ sysadmin có thể sửa trường Client status.

class ClientPolicy < ApplicationPolicy   def permitted_attributes     if user.roles.include? :sysadmin       [:name, :status]     else       [:name]     end   end end 
class ClientController < ApplicationController   def update     if @client.update_attributes(permitted_attributes(@client))     ... end 

Bạn cũng muốn show /hide trường Status trong trang điều chỉnh Client. Chỉ cần gọi phương thức permitted_attributes.

# app/views/clients/_form.html.erb <% if policy(@client).permitted_attributes.include? :status %>   <div class="form-inputs">     <%= f.input :status %>   </div> <% end %> 

Tôi đang làm việc với một phương án tốt hơn dùng CSS để hiển thị hoặc vô hiệu hóa các thuộc tính và đẩy logic vào decorator.

UI

Trong trình tạo giao diện người sử dụng erb / haml truyền thống, bạn có thể dùng kiểm tra được đề xuất trên trang Wiki của Pundit.

<% if policy(@account).update? %>    <%= đường dẫn_to "Edit account", edit_account_path(@account) %>  <% end %> 

Nhưng nếu bạn đang làm Single Page Application? Chúng tôi đã dùng ActiveModelSerializers và các phương thức thêm linh hoạt với define_method. không những vậy bạn có thể đẩy một vài hành động thông thường vào ApplicationSerializer.

class AccountSerializer < ApplicationSerializer   attributes :id, :name   ...   actions = [:index?, :show?, :new?, :create?, :edit?, :update?, :destroy?]   attributes actions   actions.each do |action|     define_method(action) do       policy = "#{object.class.name}Policy".constantize       policy.new(current_user, object).send(action)     end   end end 

Controller của bạn trả về bằng đầu ra HTML hoặc JSON.

class AccountsController < ApplicationController   def index     @accounts = Account.all     respond_to do |format|       format.html       format.json  { render  json: @accounts }     end   end end 

giờ đây, frontend application JS có thể dùng đầu ra từhttp: // localhost: 3000 / accounts.json để kiểm tra quyền và show/ hide / disable các điều khiển UI phù hợp.

[   {   id: "1",   name: "account 1",   index?: true,   show?: true,   new?: false,   create?: false,   edit?: null,   update?: null,   destroy?: false   }, ] 

Testing

Testing these policies interaction with the common RoleCheck code can get quite repetitive. That’s where stubbing can be a valuable tool. This will simulate passing user, client and roles parameters to RoleCheck and returning true or nil.
chạy thử các policies này tương tác với mã code RoleCheck bằng cách lặp đi lặp lại thao tác get. Đó là nơi có thể stubbing một công cụ có giá trị. Điều này sẽ mô phỏng các tham số người sử dụng, client và vai trò parameters để RoleCheck và trả về true hoặc nil.

# spec/policies/account_policy_spec.rb permissions :index?, :show? do   it 'valid' do     rl = double('RoleCheck', perform: true)     RoleCheck.stub(:new).with(user: user, client: client,       roles: ['admin', 'readonly_admin']).and_return(rl)     expect(subject).to permit(user, Account.new(client: client))   end   it 'invalid' do     rl = double('RoleCheck', perform: nil)     RoleCheck.stub(:new).with(user: user, client: client,       roles: ['admin', 'readonly_admin']).and_return(rl)     expect(subject).to permit(user, Account.new(client: client))   end end permissions :create?, :update?, :new?, :edit?, :destroy? do   it 'valid' do     rl = double('RoleCheck', perform: true)     RoleCheck.stub(:new).with(user: user, client: client,       roles: ['admin']).and_return(rl)     expect(subject).to permit(user, Account.new(client: client))   end   ... end 

Also checkout pundit-matchers gem.

Tài liệu tham khảo

http://blog.carbonfive.com/2013/10/21/migrating-to-pundit-from-cancan/
https://www.viget.com/articles/pundit-your-new-favorite-authorization-library
http://through-voidness.blogspot.com/2013/10/advanced-rails-4-authorization-with.html
https://www.sitepoint.com/straightforward-rails-authorization-with-pundit/
https://www.varvet.com/blog/simple-authorization-in-ruby-on-rails-apps/
https://github.com/sudosu/rails_admin_pundit

Bài viết được dịch từ nguồn
http://dmitrypol.github.io/2016/09/29/roles-permissions.html#scopes

Nguồn viblo.asia