Gem RbacCore —— 快速构建定制化的 RBAC 系统

jasl · 发布于 2017年07月16日 · 最后由 jasl 回复于 2017年07月17日 · 772 次阅读
1107

项目地址:https://github.com/jasl-lab/rbac_core

其实还是个半成品(没写测试 - -),办会嘛,工作嘛,本想会后搞,结果没忍住

RbacCore 是什么?

即使是基于 RBAC 的访问控制系统,根据项目的需要,以下考量也会影响到设计:用户可拥有单角色或者多角色?角色是否能够继承?角色是否涉及 Scope(一般出现在多租户应用中)?是否混合 ABAC(Attribute Based Access Control)?

如同和上一集的 FormCore ,RbacCore 并没有提供开箱即用的方案,仅提供了用于实现 RBAC 系统的最小公共需求,遵从了 Improving engine functionality 使其易于被扩展,以此帮助你快速构建恰好满足项目需要的访问控制系统。

RbacCore 也没有实现请求的访问控制,因为 CanCanCan、Pundit 已经经过社区多年的验证,包括其用法也是深入人心。访问控制是一个比较敏感的功能,实现这部分首先要增加很多的工作量,配套的要补充大量测试以证明其安全,此外,用户还要学习 RbacCore 提供的 Helper 和 使用方式。所以,不妨提供一种机制,能够让用户选择喜欢的访问控制库。

此外,他同样利用了 “运行时构造模型类”(在我的 FormCore 的介绍文章中有介绍)的技巧,使得前端的工作量大大减少,也更容易做 i18n。

RbacCore 提供了:

Demo

Dummy 即为 demo,预览方式参考 Readme#demo,这里演示了一个简单的任务管理应用(Project has many Tasks),用户可以拥有多个权限,访问控制使用 CanCanCan。

下文也将以此作为演示。

使用(基于 RbacCore 的 dummy app)

安装参考 Readme#Installation

创建 RbacCore 的 initializer

在项目的 config/initializers 建立 rbac_core.rb 用于编写关于 RbacCore 的配置。(以后我会写个 generator 来做这事儿)

编写用于接入访问控制 Gem 的权限描述类

以 CanCanCan 为例,如果你需要和 CanCanCan 集成,以下代码可以直接复制。

当然更好的办法是制作一个 rbac_core-cancancan gem,这样这一步(也是最难理解的一步)就可以省略了。

RbacCore 提供的 Permission 类 只包含权限的标识(name)和优先级(priority)。

CanCanCan 的 Ability 的 DSL 形如:

user.can :create, Project
user.can :read, Project, is_public: true
user.can :update, Project do |project|
  project.user_id == user.id
end

为了能够集成进 CanCanCan,需要继承 RbacCore::Permission 增加 modelblock 部分。

app/lib(强烈建议建立这个目录,并用它存放 Rails 初始化后才需要的库的代码,可以享受到线程安全的 autoload 和 修改后无需重启应用的好处) 或 lib 下建立 can_can_can_permission.rb,代码如下:

class CanCanCanPermission < RbacCore::Permission
  # CanCanCan 的 can 方法的形参 action(即第一个形参)未必和权限的标识一致
  # 考虑 编辑任务 和 编辑我的任务 这种情况
  attr_reader :action, :options

  # 父类定义的构造方法签名
  def initialize(name, priority: 0, **options, &block)
    super # 调用父类的构造方法

    @model = options.fetch(:model) # 从 options 中提取模型信息
    @action = options.fetch(:action) { name } # 若没有提供 action,则使用 name 作为 CanCanCan 需要的 action
    @options = options.except(:model, :action)
    @block = block # 储存传入的 block
  end

  # 基类的签名为 call(context, *) 故除了第一个形参 context 外,可以根据需要增加新的形参
  # context 为 CanCanCan 的 Ability 的实例
  # user 为 Ability 的构造方法的形参 user,即当前用户
  def call(context, user)
    if block_attached?
      # 因为 user 需要在每个请求处理时动态传入(即 call 的同名形参),所以我们要让 block 的第一个形参为 user 以方便注入
      # 故预期的 block 形如:{ |user, project| project.user_id == user.id }
      # 但我们需要 block 拥有 user 的信息以便做判断,但 CanCanCan 的 DSL 里不可以包含这个 user,所以要消去它
      # 于是这里使用了一个 Proc 的技巧 —— 科里化
      context.can @action, @model, &@block.curry[user]
    else
      context.can @action, @model, @options
    end
  end

  def block_attached?
    !!@block
  end
end

然后在 config/initializers/rbac_core.rb 里指定刚编写的 Permission 类

RbacCore.permission_class = CanCanCanPermission

声明项目所包含的权限

config/initializers/rbac_core.rb 里编写

RbacCore.permission_set_class.draw do
  # group 除了第一个实参外(这里即 `:project` 都将附加到其下的 `permission` 方法调用里)
  group :project, model: Project do
    # 结合 `group` 等价于 `permission :destroy, model: Project`
    permission :destroy
    permission :create, default: true # default 用于声明是否默认拥有该权限 
    permission :update
    # 调高优先级,用户拥有这一项权限的时候,保证排序后总在下一个
    # CanCanCan 的特性是,相同的 action,后声明的覆盖先前的
    # 参考 https://github.com/CanCanCommunity/cancancan/wiki/Ability-Precedence
    permission :read, priority: 1, default: true
    # 指定 action 为 :read, 这句将被翻译为 CanCanCan 的 DSL `can :read, Project, is_public: true`
    permission :read_public, action: :read, is_public: true
  end

  group :task, model: Task do
    permission :create, default: true
    permission :destroy
    permission :update, priority: 1

    permission :update_my_own, action: :update, default: true do |user, task|
      task.user_id == user.id
    end
  end
end.finalize! # finalize! 可选,意思是冻结权限声明

将 Role 和项目的“用户”挂钩

这里演示的是 User 可以有多个 Role 的情况,即多对多关系。

需要增加关联模型(迁移省略)

class RoleAssignment < ApplicationRecord
  belongs_to :user
  belongs_to :role

  validates :role,
            uniqueness: {scope: :user}
end

RbacCore::Role 已经做了 STI 支持,故可以在项目端实现 Role 模型,扩展新的行为

class Role < RbacCore::Role
  has_many :role_assignments, dependent: :destroy
  has_many :users, through: :role_assignments
end

用户(User)端代码省略

在 CanCanCan 的 Ability 中释放我们刚做的的权限定义

由于我们是用户可以对应多个角色,故需要先合并所有角色的权限,可以在 User 模型中编写如下方法

def permitted_permissions
  # role 的 permitted_permissions 方法返回去过重的排过序的 CanCanCanPermission 对象
  # 这里我们得到所有角色的 permitted_permissions 后重新做去重和排序,才能保证结果正确
  roles.map(&:permitted_permissions).flatten.uniq.sort_by(&:priority)
end

CanCanCan 的 app/models/ability.rb 中只需要编写两行即可

class Ability
  include CanCan::Ability

  def initialize(user)
    # See the wiki for details:
    # https://github.com/CanCanCommunity/cancancan/wiki/Defining-Abilities

    # 遍历我们去重、排序过的用户拥有的权限集合
    user.permitted_permissions.each do |permission|
      # 调用我们之前实现的 call 方法
      permission.call(self, user)
    end
  end
end

角色的权限管理界面

略,参见 https://github.com/jasl-lab/rbac_core/blob/master/test/dummy/app/views/roles/_permissions.html.erb

Demo 使用了 Bulma CSS 框架,仅需22行即可达到如图效果:

i18n

略,参见 https://github.com/jasl-lab/rbac_core/blob/master/test/dummy/config/locales/rbac_core.en.yml

共收到 6 条回复
136

挽尊…

9800

现实中真没必要太复杂的权限控制,太复杂了连权限配置都是个门槛。。

1107
9800pynix 回复

绝大多数系统都不用,复杂的权限控制主要受众是企业系统,其实我做的一系列 xxxCore 都是在探索这个领域。

9800
1107jasl 回复

👏 好吧

1107
9800pynix 回复

另外源码上,整个实现一百来行(当然也是因为确实做得事情少),并没有使用“魔法”,通过动态生成模型类可以极大程度复用 Rails 自身提供的机制,从而大幅减少工作量,尤其是需要和前端交互的部分(其实就是在指模型)

这里我实验性的写了一个 options_model 库来实现动态生成模型,但其实这个也不是必要的,用 Virtus ActiveType 都可以

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册