前言
最近在研究权限系统时,发现一个非常新的概念——ReBAC,同时也深入研究了一下其开源实现框架——OpenFGA。
本文将直接带领大家手把手的使用OpenFGA去实现一个鉴权功能。
本文将直接以飞书文档为例,介绍如何对其建模,如何授权,以及如何落地。OpenFGA官方提供了基础的文档建模,还有其他的一些场景实例,大家可以直接去官网看:openfga.dev/docs/modeli… 本文着重于介绍建模的思想,引导新手快速上手。
OpenFGA
github地址:github.com/openfga/ope…
官方介绍如下
一个高性能且灵活的授权/权限引擎,为开发者而生,灵感来源于 Google Zanzibar。
OpenFGA 的设计目标是帮助开发者轻松构建应用程序的权限模型,并将细粒度的授权功能集成到应用程序中。
它支持内存数据存储以便快速开发,同时支持可插拔的数据库模块。目前支持 PostgreSQL 14、MySQL 8 和 SQLite(目前为测试版)。 OpenFGA 提供 HTTP API 和 gRPC API,并拥有适用于 Java、Node.js/JavaScript、GoLang、Python 和 .NET 的 SDK。在社区部分,还可以找到第三方 SDK 和工具。它也可以作为库来使用。
同样的,如果你对于ReBAC或者OpenFGA不太熟悉的话,建议阅读前文,避免一会儿一头雾水。前文:
- 权限系统探索-权限模型、策略、ReBAC
- 权限系统探索-为什么我不推荐用OPA实现权限系统
- 关于OpenFGA:必须知道的一些概念(全网唯一中文介绍)
- 权限系统探索-ReBAC典型实践——OpenFGA与图数据库
正文
接下来,我们将会以 飞书文档 为例子,将文章分为两部分。
第一部分是推导模型,根据飞书文档的use case推理出如何建模,并且一步一步完善模型,最终实现飞书文档的权限控制。
第二部分是验证模型, 来验证模型的合理性,并验证是否可以完成飞书文档的所有功能。
第一部分看不懂也没关系(说明可能你可能还没理解ReBAC和OpenFGA),第二部分会验证此模型,到时候直接将数据摆出来,你应该就会懂了。
一、推导模型
1. 文件夹模型
1.1 父子关系
首先文件夹肯定是有父子依赖关系的,所以我们最初的建模就是如下:
type folder
relations
define parent:[folder]
这应该非常容易理解,文件夹之间可以有父子关系。
1.2 文件夹角色
从图中可以看到三个信息
- 文件夹具有
协作者的概念, - 协作者可以是个人,也可以是公司(即用户组)
- 不同的
协作者的权限可以不同——可管理,可编辑,可阅读。
那么我们就可以将权限模型拓展为如下:
type folder
relations
define parent: [folder]
define owner: [user]
define manager: [user, userGroup#member]
define writer:[user, userGroup#member]
define reader:[user, userGroup#member]
type user
type userGroup
relations
define member:[user]
目前为止,应该也非常容易理解,我们将folder分出来四个角色:owner,manager,writer,reader。并且每个角色可以授权给人或者用户组。
1.3 角色权限传递
但是我们发现一个问题:一个人如果有所有者的权限,应该自动拥有管理者,编辑者,阅读者的权限。一个人如果拥有写权限,那么一定拥有读权限。
所以我们将模型演变如下
type folder
relations
define parent: [folder]
define owner: [user]
define manager: [user, userGroup#member] or owner
define writer:[user, userGroup#member] or manager
define reader:[user, userGroup#member] or writer
type user
type userGroup
relations
define member:[user]
1.4 父子文件夹权限的传递
如果我对于一个父文件夹有权限,那么自动对于子文件夹有权限。飞书就是这样做的:
在协作者页面,可以看到,一部分协作者是继承自父级文件夹。
于是我们继续拓展模型如下:
type folder
relations
define parent: [folder]
define owner: [user]
define manager: [user, userGroup#member] or owner or manager from parent
define writer:[user, userGroup#member] or manager or writer from parent
define reader:[user, userGroup#member] or writer or reader from parent
define visitor: reader
owner应该每个文件夹只有一个人,所以不从父文件夹继承,大家自己体会一下,应该也非常容易理解。
1.5 移除或者编辑父继承过来的权限
飞书文档有一个功能,如果说我文件夹下面的某一个文件夹,要求不从父级文件夹继承,而是要有单独的权限,我应该怎么办?
飞书文档的页面如下:
修改父级文件夹传递过来的协作者的个人的权限范围(破坏掉父级文件夹继承过来的权限)
此时会弹出一个提示:
如果此时点击确认,则破坏掉了父子之间的关系,即得到如下:
我们发现,此时的每个人已经没有从父级文件夹继承的字样,并且上方会给一个提示:”已限制权限,不再继承父级文件夹的权限“。
那么问题来了!
飞书是怎么做的?
从中可以窥见,飞书大概率是采用引用的方式,子文件夹自动引用父级文件夹的协作者,如果破坏掉其中一个从父级文件夹过来的协作者,则破坏整个父子引用的关系。从而导致其他人也自动不从父级文件夹继承。
这会引发什么问题呢?——我明明是想要改一个人的权限,但是由于我改了这个人的权限,破坏了父子关系,导致其他从父文件夹继承过来的协作者也不能继承了,而是要单独的复制一份儿。 引发:
- 数据量增加:修改一个人,其他的所有继承过来的人都要复制
- 灵活性受限:我们可能需要部分人和父级文件夹一致,父级文件夹修改,子自动修改。但飞书文档无法做到。
那么我们的权限模型能够做到吗?
能! 我们可以将权限模型修改如下:(为了简洁性,将部分定义省略)
type folder
relations
define parent: [folder]
define owner: [user]
define manager_inherit: manager from parent but not block // 新加的
define manager : [user, userSet#member] or owner or manager_inherit
define block: [user, userGroup#member]
上述过程中我们添加了一个block的关系。意思为一个人可以从父文件夹继承,除了block中的人
这里的but not意思是排除的意思,如果你不太清楚定义,还是建议看前文。
(到此为止,看不懂也没关系,完善模型后,后面给你举一个实际的授权以及鉴权例子就会恍然大悟了)
这里的写法也并非唯一,也可以是:
type folder
relations
define parent: [folder]
define owner: [user]
define manager : [user, userSet#member] or owner or (manager from parent but not block)
define block: [user, userGroup#member]
使用括号也可以。
2. 文档模型
2.1 角色和操作
文档不同的地方在于文档的协作者可以绑定不同的角色,而且每个文档的角色的操作可以不一样。
也就说这是一个将角色和操作绑定的作用,那么我们需要定义一个操作的type,模型演化如下:
type folder
省略
type doc
relations
define parent: [folder, doc]
define owner: [user]
define manager_inherit: manager from parent but not block
define manager : [user, userSet#member] or owner or manager_inherit
define writer_inherit: writer from parent but not block
define writer:[user, userSet#member] or manager or writer_inherit
define reader_inherit: reader from parent but not block
define reader:[user, userSet#member] or writer or reader_inherit
define block [user, userGroup#member]
type action
relations
define allow:[doc#manager, doc#writer, doc#reader, doc#owner]
到目前为止看不懂也没关系,一会会有例子。
3. 最终的模型
最终的比较完整的模型应该是这样的
model
schema 1.1
type folder
relations
define parent: [folder]
define owner: [user]
define manager_inherit: manager from parent but not block
define manager : [user, userGroup#member] or owner or manager_inherit
define writer_inherit: writer from parent but not block
define writer:[user, userGroup#member] or manager or writer_inherit
define reader_inherit: reader from parent but not block
define reader:[user, userGroup#member] or writer or reader_inherit
define block:[user, userGroup#member]
type doc
relations
define parent: [folder]
define owner: [user]
define manager_inherit: manager from parent but not block
define manager : [user, userGroup#member] or owner or manager_inherit
define writer_inherit: writer from parent but not block
define writer:[user, userGroup#member] or manager or writer_inherit
define reader_inherit: reader from parent but not block
define reader:[user, userGroup#member] or writer or reader_inherit
define block:[user, userGroup#member]
type action
relations
define allow:[doc#manager, doc#writer, doc#reader, doc#owner]
type user
type userGroup
relations
define admin: [user]
define member: [user]
define parent: [userGroup]
但是我们发现,其实文件夹和文档的权限基本上是一模一样的,可以将两个type,folder和doc合并起来。 变成如下模型
model
schema 1.1
type doc
relations
define parent: [folder]
define owner: [user]
define manager_inherit: manager from parent but not block
define manager : [user, userGroup#member] or owner or manager_inherit
define writer_inherit: writer from parent but not block
define writer:[user, userGroup#member] or manager or writer_inherit
define reader_inherit: reader from parent but not block
define reader:[user, userGroup#member] or writer or reader_inherit
define block:[user, userGroup#member, config]
type action
relations
define allow:[doc#manager, doc#writer, doc#reader, doc#owner]
type user
type userGroup
relations
define admin: [user]
define member: [user]
define parent: [userGroup]
对于许多很抽象的东西,尤其是新概念,理解起来都比较困难,因为大脑没有形成对应的思考方式。
如果你前面都看不懂,可以直接从这里开始看,我接下来就会举例子了。
二、验证模型
1. 使用Playground
首先打开Playground
打开Playground,左上方加上我们定义好的模型,右边会自动出来对应的结构图,如下。
什么是Playground?
Playground是官方提供的调试工具,主要用于建模和验证模型,其内嵌于OpenFGA中,也就是说,启动OpenFGA就可以直接访问了。并且使用起来非常简单,属于一看就会的那种
2. 授权
我们调用接口进行授权。
OpenFGA的Api可以参照:openfga.dev/api/service
或者直接使用Playground的AddTuple
我们可以调用写入接口,批量加入测试的三元组:
{
"writes": {
"tuple_keys": [
{
"user": "folder:我的文档",
"relation": "parent",
"object": "folder:2025文档库"
},
{
"user": "folder:2025文档库",
"relation": "parent",
"object": "folder:cses文档库"
},
{
"user": "folder:cses文档库",
"relation": "parent",
"object": "folder:2025畅想"
},
{
"user": "folder:2025畅想",
"relation": "parent",
"object": "doc:消息系统2025畅想"
}
]
}
}
文件夹的示意图:
此时我们授权
{
"writes": {
"tuple_keys": [
{
"user": "user:anne",
"relation": "member",
"object": "userGroup:cses小组"
},
{
"user": "user:tom",
"relation": "writer",
"object": "folder:我的文档"
}
{
"user": "userGroup:cses小组#member",
"relation": "manager",
"object": "folder:我的文档"
}
]
}
}
此时的数据库数据如下:
3. 验证权限传递
然后,我们调用鉴权接口进行鉴权
{
"tuple_key": {
"user": "user:anne",
"relation": "reader",
"object": "doc:消息系统2025畅想"
}
}
其结果是
{
"allowed": true,
"resolution": ""
}
通过上述的鉴权,我们验证了几个权限传递:
- 用户和用户组的权限传递:我们授权cses小组拥有管理权限,anne也自动拥有了对应权限。
- 文件夹和文档的权限传递:我们授予cses小组,”我的文档”文件夹权限,而cses小组自动拥有了“消息系统2025畅想”文档的权限。
- 角色的权限传递:我们明明是授予的管理权限,但是anne也拥有了读的权限
4. 验证继承阻塞
首先我们鉴权,tom是否有 消息系统2025畅想 的权限
{
"tuple_key": {
"user": "user:tom",
"relation": "reader",
"object": "doc:消息系统2025畅想"
}
}
结果是true:
{
"allowed": true,
"resolution": ""
}
此时,如果我们移除tom访问cses文档库下面的权限。 我们可以写入三元组
{
"writes": {
"tuple_keys": [
{
"user": "user:tom",
"relation": "block",
"object": "folder:cses文档库"
}
]
}
}
此时我们鉴权:
- tom是否可以访问 消息系统2025畅想 --》 结果是false
- tom是否可以访问 cses文档库 --》结果是false
- tom是否可以访问 2025文档库 --》结果是true
也就是说,我们成功阻断了tom对cses文档库及以下的权限传递,大家可以自己去试一下。
那么,anne还可以访问吗?
我们同样进行鉴权,发现返回true。
也就是说,我们可以单独阻止某一个人从父文件夹继承,而不用担心影响其他人的权限传递。
5. 验证操作的传递
现在我们再说操作的传递,首先我们将某个文档的角色和其操作对应起来,我们进行授权:
{
"writes": {
"tuple_keys": [
{
"user": "doc:消息系统2025畅想#writer",
"relation": "allow",
"object": "action:消息系统2025畅想_move"
}
]
}
}
说明:这里的消息系统2025畅想_move就是具体的操作,但是我们不能直接放一个move在那里,我们需要区分是哪个文档的操作,所以我们再操作前面加上 文档的唯一标识(这里用的名字)作为前缀。其实 消息系统2025畅想_move 就是一个操作。
我们发起鉴权:
{
"tuple_key": {
"user": "user:anne",
"relation": "allow",
"object": "action:消息系统2025畅想_move"
}
}
其返回结果是:true
而此时我们的数据库的数据是:
只要这么多的数据,就可以支撑这么多级的权限传递,全凭借着其依据图的遍历而得来的。而相反,如果我们每个角色单独维护,角色和操作单独维护。在大规模的用户,操作上,将会是几十倍,几百倍的数据差距
相信如果你读完本文,也会惊叹于ReBAC的表现力。
总结
本文以飞书文档为例子,相信如果你看过本系列之前的文章,你就会迅速理解本文。
写本文的目的是,在和团队中其他人讨论此框架时。我发现这个框架以及概念非常的抽象,如果不结合实际的例子,大家可能无法理解如何使用,如何授权,如何鉴权。其实我后面还有一篇文章——《权限系统探索——OpenFGA深度体验报告》正在写,但是我发现,如果我讲不明白这个东西,大家看总结也会看不懂,所以临时又回来补充。
其实本人文笔有限,而且ReBAC的概念确实也比较新,如果读者还有不清楚的地方,望请评论区指正。
后面将会出一篇《权限系统探索——OpenFGA深度体验报告》用于介绍OpenFGA的优点和缺点。架构没有银弹,这个框架也不是很完美,有可取之处,但是也会有缺点。
未经本人允许,请勿转载。