前言
最近老板让我实现一个可以统一全部业务的权限系统,说实话,一开始我自信满满。但是不断深入研究后,心里就开始慌了,尤其是带入一个业务系统的实际需求,以及构建一个可拓展的一致且准确的授权系统时,就会发现这会有多么的复杂!但是毕竟东西都是人做的,慢慢搞总会搞出来的。我阅读了大量的文章,找了各种解决方案,在不断的研究以及试错之中不断的总结。本篇是我个人总结的,关于权限系统最重要的两个概念——权限模型和策略,并且还会着重介绍权限模型的新星——ReBAC。
权限模型
一提权限模型,接触过权限的开发者都不会陌生,RBAC
,ABAC
肯定都有所了解,什么ACL
,MAC
的相关概念肯定也都看过,本文主要是探讨几种能够实现通用的权限模型RBAC
,ABAC
,ReBAC
。
三种权限模型介绍
目前可供我们选择的主要就是三种权限模型:
- 基于角色的访问控制(RBAC)
- 基于属性的访问控制(ABAC)
- 基于关系的访问控制(ReBAC)
但是详细的介绍这三种模型并不是本文的重点 如果需要查看相关的概念可以看一下这些文章:
- B端权限规则模型:RBAC模型
- www.strongdm.com/blog/rbac-v…
- javaguide.cn/system-desi…
- ReBAC:兼容DDD的下一代授权模型
- www.authing.cn/blog/532
三种权限模型对比的结论如下:
RBAC:传统的基于角色
的权限控制
优点:
- 简单易用
- 集中化管理
缺点:
- 灵活性有限,并且无法支持动态条件(比如IP、时间)
- 对于粒度较小的资源会产生角色膨胀问题
- 不适合复杂的关系场景(资源层次,关系传递)
ABAC:基于属性
的权限控制
优点:
- 高度灵活
- 精细化控制
- 支持环境属性
缺点:
- 复杂性非常高
- 实施评估时,会带来性能瓶颈
- 依赖属性(资源)数据
ReBAC:基于关系
的权限控制
优点:
- 适合复杂资源关系
- 非常适合协作场景
- 更符合实际业务需求
缺点:
- 复杂查询会有性能瓶颈,需要依赖数据库优化能力。
- 依赖关系建模,需要明确。
我的选择
目前我们需要的是一个通用的权限系统,需要精细化到具体的资源。那么首先RBAC
就被排除在外了,因为传统的RBAC
会产生角色爆炸问题,并且在处理复杂的权限传递时,其授权将会非常的复杂。
ABAC
因为其实践的复杂度,较难的统一策略,较大的维护成本(各个业务需要变更属性时)也被我们排除在外,并且其在复杂场景下的性能问题,也是一个需要考虑的原因。相关内容可以看我上一篇文章《权限系统探索-为什么我不推荐用OPA实现权限系统》
所以我们选择一个比较新的概念-ReBAC
!
ReBAC
是基于关系的,在实际业务中非常适合权限的传递(下文会举例子),并且其模型的定义也会比类似于ABAC的策略的学习成本低(下文也会举例子)。尤其是目前我们在做一个协同的APP,这将很好的帮我们管理如组织架构,文件,表单等内容的管理。
接下来,我们将引入第二个很重要的概念——策略或者说模型
策略 or 模型
什么是策略?
我对于策略的定义是:定义访问控制规则和资源关系的结构
比如在实际业务中,如果权限由各个业务分散处理,那么业务系统就要为之定义一个if else
语句,如下:
if(Object.permissionList.contain(user.permission)||
Object.permissionRole.contain(user.role)){
业务实际处理逻辑
} else{
无权访问
}
其实这个判断语句不太严格符合我对策略的定义,它只定义了访问控制规则。
它描述了user
之间的关系:用户可以通过用户组(Role) 进行授权,从而获得权限。
但是这里并没有定义资源关系,实际的业务场景比这个要复杂得多。比如一个授权逻辑下,一个人对于一个文件夹有owner
权限,我们将会自动拥有此文件夹下的所有文件owner
的权限。类似这样的权限传递,除非我们直接进行一个赋予权限,类似于ACL表,否则会比较难直接处理。
如何定义一个策略|模型
这个问题可以转化为,如何定义一个资源的控制规则和资源关系。
策略要求:
- 支持复杂业务
- 支持有层级的权限
- 支持权限本身的传递。
- 支持权限和资源分离,不维护业务数据。
- 定义尽量简单
- 能够提供给业务人员直接定义,不要有太高的学习成本。
- 复杂的策略容易出错,不易排查问题,在权限变更时,难以维护。
- 调试要简单。
- 策略管理要简单(权限系统应该维护所有业务系统的策略)。
大家可以立马想到:基于关系
的权限模型可以很好的描述资源关系的结构以及权限传递。
但是ReBAC中并没有一个策略
的概念,但与之对应的,在ReBAC
中的定义资源,用户组,关系的名词称之为模型。所以,我将ReBAC中的模型也理解为一种策略。
模型
上文提到,我将模型定义为一种策略,是因为他描述了访问控制规则和资源关系结构,大家可能看不懂这句话。下文,我将直接代入OpenFGA中模型定义的例子,让大家更容易理解ReBAC中模型的概念。
关于OpenFGA
OpenFGA是现在比较流行的ReBAC实践,受到Google Zanzibar的启发。它将强大的基于关系的访问控制 (ReBAC)和基于属性的访问控制 (ABAC)概念与特定于领域的语言相结合。
OpenFGA的一些概念
在开始下文的举例之前,你应该了解一些OpenFGA中的概念,具体的概念可以看一下官网的一些概念,或者查看我的翻译的文章,我的文章中也加入了一些个人理解以及示例。
以文档为例,我们定义一个模型
下面的例子,我是直接扒的OpenFGA官网的示例,加以解释,应该能比较浅显易懂的介绍模型如何实现!
官网示例的链接:openfga.dev/docs/modeli…
下面直接开始!
这个我认为比较浅显易懂,我们定义了一个资源document,一个用户类型的的 user。
user可以对document有owner,writer,commenter,viewer权限。其实也可以细分粒度为can_write, can_view, can_comment等,实际关系定义可以结合业务需要。
有了这样的模型,我们就可以直接授权某人对于某资源拥有对应的权限。
这里就是描述,user
类型的用户beth
对于document类型
的id为2021-budget的文档有可评论的权限。这种授权基本就实现了用户对于资源的访问控制,类似于ACL表。
但是发现此模型有一个问题,作为文章的拥有者,那他应该也是文章的写作,评论,和查看者。于是我们需要修改模型:
但是由于我直接分享到个人,对于权限更好的管理,我们应该有一个用户组的概念,我们将权限分给用户组,用户组下的人就会自动用于对应的权限。
这里的domain
可以理解为一个用户组,或者用户集。我们就可以很方便的将权限授予用户组,此用户组下成员自动拥有对应权限,类似于RBAC
基于这种授权,用户组xyz下的成员anne,beth,charles通过用户组的权限传播,自动拥有了viewer的权限。
假设我们还有一个需求,一个人对于某文档有了权限,那么就自动对于此文档下面的子文档有了权限,可以修改为如下。
首先我们需要加入一个文档的父子关系:
然后我们要做一个权限传播
我们的文章可以是公开的怎么办?它也是可以支持通配符,如user:*
,user:*
很容易理解,就是user类型的所有人.
甚至可以通过关系实现黑名单和ip访问限制(IP访问限制不一定要像我这样,可以使用条件元组)
model
schema 1.1
type user
type document
relations
define owner: [user, domain#member] or owner from parent
define writer: [user, domain#member] or owner or writer from parent
define commenter: [user, domain#member] or writer or commenter from parent
define viewer: [user, user:*, domain#member] or commenter or viewer from parent
define parent: [document]
define approved_ip_range: [ip-address-range] // 新增:定义允许访问的IP范围
define blackList [user] //黑名单
type domain
relations
define member: [user]
type ip-address-range
relations
define approved_range: [user] // 新增:定义哪些用户可以通过特定IP访问资源
通过上述的例子,我们就实现了用户层级(用户,和用户组),资源层级(资源的父子关系),权限传播(不同权限的传播(write对view),父子资源的权限传播,用户之间的权限传播)。甚至于可以通过关系,你能够实现不同类型的资源之间的权限传递。比如一个人对某文档有了权限,就自动拥有文档对应的任务的权限。
其实本质上这种资源上的权限传递,反过来也是对于我们资源鉴权的多层判断。 如果根据上面这个例子,我们需要判断
- 用户是否有资源的直接权限
- 用户是否有某资源的间接权限(拥有writer,那么判断是否拥有viewer就是true)
- 用户是否在有权限的某用户组中
- 资源是否公开。
我们无需做这种判断,因为我们已经给予OpenFGA的模型,它会基于图的思想自动推导出是否存在用户到资源的路径。我们要做的就是给一个起始点user:userId,终点:document:documentId。
上文中我提到了ABAC的典型实践OPA,那么我们通过OPA将如何去定义一个类似的策略,实现同样的一个文档的鉴权功能。
以下是我用ChatGpt试图实现和这个功能差不多的OPA策略,以下是ChatGpt生成的内容:
package example
# 用户类型
type_user = "user"
# 文档类型
type_document = "document"
# 域类型
type_domain = "domain"
# IP地址范围类型
type_ip_range = "ip-address-range"
# 父文档-子文档关系
document[parent] = document {
document[parent] = input.document_parent
}
# 用户与域成员关系
domain[member] = user {
domain[member] = input.domain_member
}
#输入数据
document[owner] = user {
input.document_owner == user
}
document[writer] = user {
input.document_writer == user
}
document[commenter] = user {
input.document_commenter == user
}
document[viewer] = user {
input.document_viewer == user
}
# 权限传递:如果用户有写权限,则自动获得评论和查看权限
allow_permission_with_transitive_rights(user, doc, perm) {
# 如果用户有写权限,则自动获得评论和查看权限
perm == "writer"
allow_permission(user, doc, "writer")
allow_permission(user, doc, "commenter")
allow_permission(user, doc, "viewer")
}
# 权限检查,递归父文档的权限,考虑用户和域的关系
allow_permission(user, doc, perm) {
# 直接关系
document[perm][user] # 用户是否直接有该权限
# 如果没有直接权限,递归查找父文档的权限
not document[perm][user]
parent_doc := document[parent]
allow_permission(user, parent_doc, perm) # 递归父文档权限
}
# 检查用户是否属于某个域
user_belongs_to_domain(user, domain) {
domain[member][user]
}
# 权限传递的详细实现
allow_permission_for_user_in_domain(user, doc, perm) {
# 检查用户是否属于与文档相关的域
domain_membership := domain[member][user]
domain_membership
# 检查文档的权限
allow_permission(user, doc, perm)
# 权限传递
allow_permission_with_transitive_rights(user, doc, perm)
}
# IP访问控制,检查IP是否在允许的范围内
allow_ip_access(user, ip) {
ip_range := input.ip_range
ip_range[approved_range][user]
}
# 黑名单检查,判断用户是否在黑名单中
blacklist(user) {
input.blacklist[user]
}
# 综合权限检查
allow_access(user, doc, perm, ip) {
# 权限检查,包括父文档继承
allow_permission_for_user_in_domain(user, doc, perm)
# 黑名单检查
not blacklist(user)
# IP访问权限检查
allow_ip_access(user, ip)
}
其实我没有验证过这个策略,但是这个策略一看就不能实现我们上述的模型,即使我通过ChatGpt生成过多次,也无法提供一个相应功能的策略。因为它没有考虑到input
的数据应该包含用户的用户组数据,资源的父层级数据,所以根本就不可能实现通过这些数据来判断。
如果读者能够给出一种相对应的OPA实现方式,我将感激不尽。
通过OpenFGA的模型与OPA的策略对比:
- OpenFGA的层级关系直接,而OPA的层级关系以及权限传递复杂。
- OpenFGA对于业务人员更加的友好,低学习成本。
- OpenFGA利于调试(可以逐层使用listObjects),OPA基于Rego,递归权限时出问题不利于调试。
结论
本文通过三种权限的对比,结合它们实际的应用框架,找到了适合我们权限系统的权限模型-ReBAC。我们可以通过ReBAC实现更加复杂的权限管理,并且提供给业务开发人员较低的学习成本。
后续也会探讨如何去落地ReBAC,以及OpenFGA相关原理的内容。
补充
刚刚看到有人评论区提了问题,我认为也是本文疏漏的地方,非常感谢。因为评论区的代码没有格式,我就直接补充到正文中。
问题如下:
问题1:父子文档如何定义?
在定义完模型之后,我们就要向具体的模型去添加三元组(user,relation,Object),这样才能将我们具体的人和具体的资源进行连接,比如:
user -> user:beth
relation -> viewer
object -> document:2021-budget
这个例子是说,id为beth的用户对id为2021-budget的document有可读权限。但ReBAC模型中的三元组(user,relation,Object)并非只能理解为用户和资源的关系,这里的user和Object相当于是主体和客体,上述这个父子文档定义的问题就可以维护进去一个三元组:
user -> document:2021-budget
relation -> parent
object -> document:2024-note
此处的定义相当于是文档2021-budget
是文档2024-note
的父文档。
假定现在有一个逻辑:某人拥有了某父文档的可读权限,自动拥有子文档的可读权限。 那么模型就可以是:
type document
relations
define viewer: [user] or viewer from parent
按照刚刚那个例子,我们可以在授权的时候向权限系统(如OpenFGA)维护两个元组
user -> user:beth
relation -> viewer
object -> document:2021-budget
user -> document:2021-budget
relation -> parent
object -> document:2024-note
此时如果我们判断user:beth
对document:2024-note
是否有可读权限,此时将会返回true
。
结论1
- 三元组中的user和Object并非是用户和资源,而是主体和客体,相当于图的两个节点,relation是边。我们需要向ReBAC权限系统(如OpenFGA)中维护,用户和用户集的三元组,用户(或用户集)和资源的三元组,资源和资源的三元组。鉴权时,我们只需要两个节点,即可根据图的遍历,找到是否有对应可达的关系,从而判断是否有权限。
- 我们本身只是想要维护用户和资源的关系(是否有权限),为什么还要去维护用户和用户集,资源和资源的关系?这是因为,对于有层级的资源,比如典型的文档,层级的权限传播是非常有必要的。由于我们对父文档有了xxx权限,所以我们对于子文档有了xxx权限。而不使用推导关系,直接授权某个人和某资源的权限的话,就会退化为ACL表。我们就需要循环然后递归给父文档下的所有子文档添加权限,而这个操作只能是业务系统去做,但是我们需要给予业务系统较少的维护权限的成本,这才是权限系统单拎出去的作用。
问题2:RBAC角色膨胀 or 策略膨胀?
其实角色膨胀问题的本身就将精细度的权限复杂化,本来我直接可以给某人某个资源的的权限,但是使用了RBAC,就使得我们需要为这个权限本身创建一个角色。
常规的RBAC并不适合对于粒度小的资源的控制,但是可以加上用户和资源的直接访问来解决这个问题。其实就是少维护了角色本身。转转团队本身就是这样做的,可以看一下这篇文章: 转转统一权限系统的设计与实现(设计篇)
策略会膨胀吗?
你说的策略膨胀,应该是类似于Authing(国产的权限验证框架),转转这种,针对于每一个资源单独设置一个策略,这是因为他们针对于ABAC
的权限模型,需要为每个资源的属性去建立策略。但是ReBAC而言,策略叫做模型,每一种资源拥有同一种模型。
如果使用ReBAC,这里的策略(模型)本身并不会膨胀,比如一个文档的模块,我们只需要定义一个文档的model。比如工作流,我们也只需要定义一个model。某个类型业务模块只需要定义一个model即可,我的理解是你是否是想问,三元组的膨胀问题?比如还要维护用户关系,资源关系等。
其实这确实会导致三元组的数量会很多,但是一定要记住——好的授权大于鉴权,如上面说的,当我授权父文档时可读权限时,子文档可以自动拥有权限,好的模型定义可以提升大量的开发效率。我后面也会单开文章去探索模型定义的最佳实践,三元组本身也可以降低业务人员对权限的理解难度(相对于OPA的策略而言)。
至于性能问题,三元组的存储和维护,使用传统的关系型数据库即可,OpenFGA官方推荐使用Pg,为什么不使用图数据库?使用传统关系型数据库,隐含关系又是怎么推出来的,这就是我后面一篇文章要讲的内容。
一定要相信关系型数据库的查询能力,比如三元组,加上索引以后,千万级的数据也可以是毫秒级别。
最后,非常感谢这位jy的评论,如果还有没有讲清楚的地方,望请指正。