权限系统探索-ReBAC框架——OpenFGA实战

2,358 阅读12分钟

前言

最近在研究权限系统时,发现一个非常新的概念——ReBAC,同时也深入研究了一下其开源实现框架——OpenFGA。 本文将直接带领大家手把手的使用OpenFGA去实现一个鉴权功能。

本文将直接以飞书文档为例,介绍如何对其建模,如何授权,以及如何落地。OpenFGA官方提供了基础的文档建模,还有其他的一些场景实例,大家可以直接去官网看:openfga.dev/docs/modeli… 本文着重于介绍建模的思想,引导新手快速上手。

OpenFGA

github地址:github.com/openfga/ope…

官网:openfga.dev/docs/fga

官方介绍如下

QQ_1734409882514.png

一个高性能且灵活的授权/权限引擎,为开发者而生,灵感来源于 Google Zanzibar。

OpenFGA 的设计目标是帮助开发者轻松构建应用程序的权限模型,并将细粒度的授权功能集成到应用程序中。

它支持内存数据存储以便快速开发,同时支持可插拔的数据库模块。目前支持 PostgreSQL 14、MySQL 8 和 SQLite(目前为测试版)。 OpenFGA 提供 HTTP API 和 gRPC API,并拥有适用于 Java、Node.js/JavaScript、GoLang、Python 和 .NET 的 SDK。在社区部分,还可以找到第三方 SDK 和工具。它也可以作为库来使用。

同样的,如果你对于ReBAC或者OpenFGA不太熟悉的话,建议阅读前文,避免一会儿一头雾水。前文:

正文

接下来,我们将会以 飞书文档 为例子,将文章分为两部分。

第一部分是推导模型,根据飞书文档的use case推理出如何建模,并且一步一步完善模型,最终实现飞书文档的权限控制。

第二部分是验证模型, 来验证模型的合理性,并验证是否可以完成飞书文档的所有功能。

第一部分看不懂也没关系(说明可能你可能还没理解ReBACOpenFGA),第二部分会验证此模型,到时候直接将数据摆出来,你应该就会懂了。

一、推导模型

1. 文件夹模型

1.1 父子关系

首先文件夹肯定是有父子依赖关系的,所以我们最初的建模就是如下:

type folder
  relations
    define parent:[folder]

这应该非常容易理解,文件夹之间可以有父子关系。

1.2 文件夹角色
24fd6576c65ca9dbafac99c2eadb18e5.png

从图中可以看到三个信息

  1. 文件夹具有协作者的概念,
  2. 协作者可以是个人,也可以是公司(即用户组)
  3. 不同的协作者的权限可以不同——可管理,可编辑,可阅读。

那么我们就可以将权限模型拓展为如下:

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 父子文件夹权限的传递

如果我对于一个父文件夹有权限,那么自动对于子文件夹有权限。飞书就是这样做的:

QQ_1737272889929.png 在协作者页面,可以看到,一部分协作者是继承自父级文件夹。

于是我们继续拓展模型如下:

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 移除或者编辑父继承过来的权限

飞书文档有一个功能,如果说我文件夹下面的某一个文件夹,要求不从父级文件夹继承,而是要有单独的权限,我应该怎么办?

飞书文档的页面如下:

修改父级文件夹传递过来的协作者的个人的权限范围(破坏掉父级文件夹继承过来的权限)

QQ_1737273208913.png

此时会弹出一个提示:

QQ_1737273292029.png

如果此时点击确认,则破坏掉了父子之间的关系,即得到如下:

QQ_1737273343180.png 我们发现,此时的每个人已经没有从父级文件夹继承的字样,并且上方会给一个提示:”已限制权限,不再继承父级文件夹的权限“。

那么问题来了!

飞书是怎么做的?

从中可以窥见,飞书大概率是采用引用的方式,子文件夹自动引用父级文件夹的协作者,如果破坏掉其中一个从父级文件夹过来的协作者,则破坏整个父子引用的关系。从而导致其他人也自动不从父级文件夹继承。

这会引发什么问题呢?——我明明是想要改一个人的权限,但是由于我改了这个人的权限,破坏了父子关系,导致其他从父文件夹继承过来的协作者也不能继承了,而是要单独的复制一份儿。 引发:

  1. 数据量增加:修改一个人,其他的所有继承过来的人都要复制
  2. 灵活性受限:我们可能需要部分人和父级文件夹一致,父级文件夹修改,子自动修改。但飞书文档无法做到。

那么我们的权限模型能够做到吗?

能! 我们可以将权限模型修改如下:(为了简洁性,将部分定义省略)

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 角色和操作

文档不同的地方在于文档的协作者可以绑定不同的角色,而且每个文档的角色的操作可以不一样。

QQ_1737274437724.png

也就说这是一个将角色和操作绑定的作用,那么我们需要定义一个操作的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,左上方加上我们定义好的模型,右边会自动出来对应的结构图,如下。

QQ_1737275507418.png
什么是Playground?

Playground是官方提供的调试工具,主要用于建模和验证模型,其内嵌于OpenFGA中,也就是说,启动OpenFGA就可以直接访问了。并且使用起来非常简单,属于一看就会的那种

QQ_1737275644901.png

2. 授权

我们调用接口进行授权。

OpenFGA的Api可以参照:openfga.dev/api/service 或者直接使用PlaygroundAddTuple

我们可以调用写入接口,批量加入测试的三元组:

{
        "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畅想"
                        }
                ]
        }
}

文件夹的示意图:

QQ_1737276122104.png

此时我们授权

{
        "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:我的文档"
                        }
                      ]
        }
}

QQ_1737277411980.png

此时的数据库数据如下:

QQ_1737279840633.png

3. 验证权限传递

然后,我们调用鉴权接口进行鉴权

{
  "tuple_key": {
    "user": "user:anne",
    "relation": "reader",
    "object": "doc:消息系统2025畅想"
  }
}

其结果是

{
    "allowed": true,
    "resolution": ""
}

通过上述的鉴权,我们验证了几个权限传递:

  1. 用户和用户组的权限传递:我们授权cses小组拥有管理权限,anne也自动拥有了对应权限。
  2. 文件夹和文档的权限传递:我们授予cses小组,”我的文档”文件夹权限,而cses小组自动拥有了“消息系统2025畅想”文档的权限。
  3. 角色的权限传递:我们明明是授予的管理权限,但是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文档库"
                        }
                ]
        }
}

QQ_1737277817163.png

此时我们鉴权:

  1. tom是否可以访问 消息系统2025畅想 --》 结果是false
  2. tom是否可以访问 cses文档库 --》结果是false
  3. 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

QQ_1737278460336.png

而此时我们的数据库的数据是:

QQ_1737278516754.png

只要这么多的数据,就可以支撑这么多级的权限传递,全凭借着其依据图的遍历而得来的。而相反,如果我们每个角色单独维护,角色和操作单独维护。在大规模的用户,操作上,将会是几十倍,几百倍的数据差距

相信如果你读完本文,也会惊叹于ReBAC的表现力。

总结

本文以飞书文档为例子,相信如果你看过本系列之前的文章,你就会迅速理解本文。

写本文的目的是,在和团队中其他人讨论此框架时。我发现这个框架以及概念非常的抽象,如果不结合实际的例子,大家可能无法理解如何使用,如何授权,如何鉴权。其实我后面还有一篇文章——《权限系统探索——OpenFGA深度体验报告》正在写,但是我发现,如果我讲不明白这个东西,大家看总结也会看不懂,所以临时又回来补充。

其实本人文笔有限,而且ReBAC的概念确实也比较新,如果读者还有不清楚的地方,望请评论区指正。

后面将会出一篇《权限系统探索——OpenFGA深度体验报告》用于介绍OpenFGA的优点和缺点。架构没有银弹,这个框架也不是很完美,有可取之处,但是也会有缺点。

未经本人允许,请勿转载。