本文将介绍:在 Rails 应用中,如何使用 Pundit 实现一个基于角色的授权系统。
引入角色是为了让用户可以动态管理权限,让权限点以集合的形式分配给用户,如果没有这样的需求,可以考虑其他简单的方案。
+------+ +------+ +------------+
| | N N | | 1 N | |
| User |<-------->| Role |<-------->| Permission |
| | | | | |
+------+ +------+ +------------+
User 和 Role 是多对多的关系,Role 和 Permission 是一对多的关系。Permission 的定义包含两个字段:action
和 resource
, 分别与 Rails 的 Controller Action 和 Model 对应。
Model 关系定义:
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
has_many :permissions, through: :roles
end
class Role < ActiveRecord::Base
has_many :permissions, dependent: :destroy
has_and_belongs_to_many :users
end
class Permission < ActiveRecord::Base
belongs_to :role
has_many :users, through: :role
end
为了方便在角色管理时能够列出可选择的权限点,权限点的定义需要通过某种方式存储起来:
class ApplicationPolicy
class << self
def actions
@actions ||= []
end
def permit(action_or_actions)
acts = Array(action_or_actions).collect(&:to_s)
acts.each do |act|
define_method("#{act}?") { can? act }
end
actions.concat(acts)
end
end
private
def can?(action)
permission = {
action: action,
resource: record.is_a?(Class) ? record.name : record.class.name
}
user.permissions.exists?(permission)
end
end
在 ApplicationPolicy
里定义一个 permit
方法 (类方法) 用来定义和保存权限点,can?
方法用来做权限检查。
然后就可以像这样声明权限点:
class ResourcePolicy < ApplicationPolicy
permit [:read, :create, :update, :destroy]
end
这些 Action 就会被保存到 ResourcePolicy.actions
里。
另外还需要两个方法 policies
和 resource
:
class ApplicationPolicy
class << self
def policies
@policies ||= Dir.chdir(Rails.root.join('app/policies')) do
Dir['**/*_policy.rb'].collect do |file|
file.chomp('.rb').camelize.constantize unless file == File.basename(__FILE__)
end.compact
end
end
def resource
name.chomp('Policy')
end
end
end
分别用来获取所有的 Policy 和 每个 Policy 对应的 resource (这两个方法是通过简单的命名规则实现的,灵活性会差一点).
在角色管理中,可以像这样列出所有可选择的权限点:
<% ApplicationPolicy.policies.each do |policy| %>
<% resource = policy.resource %>
<div>
<span><%= resource %></span>
<% policy.actions.each do |action| %>
<% checked = role.permissions.exists?(action: action, resource: resource) %>
<% value = "#{action}##{resource}" %>
<%= f.check_box :permissions, { multiple: true, checked: checked }, value, nil %>
<%= f.label :permissions, value, value: value %>
<% end %>
</div>
<% end %>
在用户管理中,可以这样为用户指定角色:
<div>
<%= f.label :roles %><br />
<%= f.collection_check_boxes :role_ids, Role.all, :id, :name %>
</div>
这个系统的完整实现请参考此项目 (mxyzm/oh_my_user) 的后台管理部分。
原文地址:http://www.xyyz.me/2015/07/24/role-based-authorization-with-pundit.html