Role-based Authorization with Pundit

本文将介绍: 在 Rails 应用中, 如何使用 Pundit 实现一个基于角色的授权系统.

数据模型

+------+          +------+          +------------+
|      |  N    N  |      |  1    N  |            |
| User |<-------->| Role |<-------->| Permission |
|      |          |      |          |            |
+------+          +------+          +------------+

User 和 Role 是多对多的关系, Role 和 Permission 是一对多的关系. Permission 的定义包含两个字段: actionresource, 分别与 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 里.

另外还需要两个方法 policiesresource:

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) 的后台管理部分.

参考资料