Roles permission - Chuyển từ Cancancan sang Pundit
Gần đây ứng dụng 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 triển 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ự tích hợp tuyệt vời với RailsAdmin, Devise và các gem ...
Gần đây ứng dụng 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 triển 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ự tích hợp tuyệt vời với RailsAdmin, Devise và các gem khác. Tất cả các quyền được định nghĩa trong tệp ability.rb nhưng cùng với thời gian thì file này có thể phát triển khá lớn. Ngoài ra còn có các nhược điểm khác như không có khả năng xác định cấp phép mức trường dữ liệu 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.
Thông thường bạn có rất nhiều model mà cần phải chia sẻ cùng quyền. Vì vậy, có thể bạn không muốn tạo ra policy files cho từng 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 sử dụng Mongoid nhưng thiết kế tương tự 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 trường hợp 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ó nguồn gốc hình thức 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 từng Client, Account và Company và sau đó bạn sẽ không cần phải làm 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 cần thiết.
class ClientPolicy < CommonPolicy ... end class AccountPolicy < CommonPolicy ... end class CompanyPolicy < CommonPolicy ... end
Để cho mọi thứ đơn giản chúng 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ể chỉnh sửa 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 tài khoản và company_admin thể làm tương tự 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.
Ngoài ra hệ thống còn có vai trò mở rộng (cho người dùng nội bộ) được xác định trực tiếp trên User model. Chỉ người dùng nội bộ có thể tạo / hủy tạo ra client mới nhưng Client Admins có thể thay đổi 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 cấp cho người dùng nội bộ truy cập 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 ứng dụng lớn nhưng client phải được đặc tả một cách rõ ràng hơn. Ngoài ra khi chúng tôi đang trong show, edit, update hoặc destroy chúng ta có thể lấy được client từ các bản ghi. Trong số đó chúng 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 truy cập 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 truy cập 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ì thế chúng ta có thể trích xuất nó vào lớp riêng biệt.
# 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ể sử dụng gem Rolify để trỏ users tới roles nhưng chúng ta đã có model UserClient vì những lý do khác vì vậy chúng ta thừa hưởng điều đó.
Bạn bắt đầu với :index?, :show? ... nhưng tiếp theo chúng ta cần định nghĩa thêm các quyền mà chúng ta muốn điểu chỉnh. Chúng 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 theo bạn gọi xác thực.
Cá nhân tôi thích yêu cầu xác thực cho tất cả các hành động trong controller ngay cả khi tôi đặt def index? true; Kết thúc để cho mọi người truy cập.
class AccountsController < ApplicationController after_action except: [:index] { authorize @account } after_action only: [:index] { authorize @accounts } end
Giả sử bạn có trường report_admin cho phép người 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 <%= link_to('Dashboard', dashboard_index_path) if policy(:dashboard).index? %> |
Kiểm tra đảm bảo 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à vấn đề của github.
Pundit::NotDefinedError at /dashboard unable to find policy `DashboardPolicy` for `:dashboard`
Internal users có thể xem tất cả các records, client specific users chỉ có thể xem các account và company được phân bổ 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
Đôi khi bạn cần phải xác định quyền truy cập 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 chỉnh sửa 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 giải pháp tốt hơn sử 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.
Trong trình tạo giao diện người dùng erb / haml truyền thống, bạn có thể sử dụng kiểm tra được đề xuất trên trang Wiki của Pundit.
<% if policy(@account).update? %> <%= link_to "Edit account", edit_account_path(@account) %> <% end %>
Nhưng nếu bạn đang xây dựng Single Page Application? Chúng tôi đã sử dụng ActiveModelSerializers và các phương thức thêm linh hoạt với define_method. Thậm chí bạn có thể đẩy một số 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
Bây giờ, frontend application JS có thể sử 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 thích hợp.
[ { id: "1", name: "account 1", index?: true, show?: true, new?: false, create?: false, edit?: null, update?: null, destroy?: false }, ]
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. Thử nghiệm 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 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.
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