Roles permission - Chuyển từ Cancancan sang Pundit
Bài đăng này đã không được cập nhật trong 7 năm
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.
Grouping policies
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
Mapping roles to permissions
Để 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 đó.
Beyond RESTful actions
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.
Require authorize in application controller for all actions
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
Headless policies
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`
Scopes
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
Field level permissions
Đô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.
UI
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
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.
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
All rights reserved