背景
在使用OpenFGA做权限系统时,我们有一个必须要面对的问题——Search With Permissions。 这个问题作为OpenFGA官方文档的最后一篇文档,这个问题也是相当的有复杂度。
问题描述
前提: 权限系统数据和业务数据分离
问题: 使用搜索时,怎么保证用户查出的是自己有权限的资源,同时保证性能。
为什么会遇到这个问题呢?
由于OpenFGA的数据不依赖资源的数据,官方提供了三种解决方案:
- 方案一: 先通过OpenFGA查询出有权限的数据,然后将id放到用户查询的sql或者搜索引擎中。
- 方案二: 先查询资源数据,然后使用OpenFGA过滤掉没有权限的数据。
- 方案三: 将OpenFGA中的三元组展开为Acl表,然后和资源表关联。
问题在于,虽然有了这三种方式,但是针对于数据量的大小,每种方式使用的场景非常有限,并且实现难度也不一样。
接下来我们逐个分析。
方案一: Build a list of IDs, then search
首先通过OpenFGA构建一个有权限的Id列表,然后在业务系统中搜索。
如何构建有权限的Id列表?
OpenFGA提供了一个list Object的API。 这个API可以输入用户的key,然后返回所有的资源的key(也就是id列表)。
方案一的局限性
性能的瓶颈就在于list Object这个API上面。
当用户可以访问的数据量较小(大约1000条)且用户可以访问的数据占比比较低时,可以使用这种方式。因为OpenFGA通过路径判断,找到这些有权限的资源,性能其实非常地好。
而一旦用户可以访问的数据量非常大,或者用户可以访问的数据占比比较高时,list Object就会浪费大量的性能去列举有权限的资源,而绝大多数资源都是有权限的,那么列举这些资源的时间就会显著的增加。
方案二:Search, then check
首先查询资源,然后判断用户是否有这些资源的权限。
方案二的局限性
方案二的局限性和方案一恰恰相反。
对于用户而言,如果绝大多数的资源都有权限,那么就会出现,比如我需要查询10个有权限的资源,我只需要通过业务数据分页查出10个数据,然后去判断这些数据是否都有权限(判断10次),假设这10个数据中有2个没有权限,那么可以再去查一次业务数据,然后判断得到2个有权限的资源,补充到分页中。
方案二的局限性就在于,用户可以访问的数据很少时,就会出现极端情况:每一次都没有查询到有权限的资源,那么这10个的分页就永远无法补充完成。甚至于用户有权限的资源都不到10个,那么就会将所有搜索出来的资源都会check一遍,数据量大时,性能将会非常的糟糕。
方案三:将权限系统数据退化为Acl表,然后表关联
还有一种特殊场景:用户可以访问大量的数据,但占总数的百分比很低。
比如文档资源,或者公司内部的一些资料,用户可以访问10万个资源,但是我们的数据库可能会有上千万的资源(多租户场景下)。
无论是方案1还是方案2针对于这种情景,性能都不会特别的好,我们只能另寻他路。
使用Acl表
其实做过权限相关内容的开发者,对于Acl一定不陌生。方案三的方式就是通过Acl表,快速列出用户有权限的资源id,然后和业务数据进行取交集或者和业务表关联,从而得到有权限的资源。
官方提供了实现方案三的一种思路:
-
自己维护一个ACL的三元组表
-
使用changes API监听权限变更的三元组。
-
然后将变更的三元组展开,变为最基础的三元组存入我们自己维护的三元组表里面。
为什么要展开?
这就关乎到OpenFGA的特点了,OpenFGA基于关系,通过model,同一个三元组数据可以表示多个信息,比如:
{
"user":"user:tom",
"relation":"writer",
"object":"doc:我的文档"
}
基于我们自己定义的模型,可以表现为用户是我的文档的writer,同时用户是我的文档的reader(基于模型的关系传递)
如果你不清楚的话,建议你看一下本系列的前文。
所以展开就是将这样的三元组拓展为其含义的所有三元组,避免需要OpenFGA的推理过程。 上述的三元组就要展开为:
{
"user":"user:tom",
"relation":"writer",
"object":"doc:我的文档"
},
{
"user":"user:tom",
"relation":"reader",
"object":"doc:我的文档"
}
方案三的局限性
方案三的局限性就在于实现难度非常的高,同时无法保证某一时间,权限的一致性。
首先我们不能简单的将三元组展开,即得到ACL。因为这样,数据量将会非常的爆炸,对于一个三元组的展开结果的数量将是其原本的数倍。而假设如果我们有以下的三元组(文档公开的情况下):
{
"user":"user:*",
"relation":"writer",
"object":"doc:我的文档"
}
那么数据量就会从1条变为所有的人员的数量。
其次,由于是监听权限变更,那么一定会导致当前的权限系统变更后,过了一段时间,用户才真正的可以访问文档或者查看不到文档。
使用位图
如果你也研究过权限相关的内容,看到这里一定会提出质疑,Acl表不是一般都是使用位图实现吗?
没错,使用位图对于数据量会有极大的数据缩减,可以将一个文章所有可以访问的用户,或者一个用户所有可以访问的文章浓缩到一个数据中,即位图。我们可以使用位图来解决数据量问题。
因为我们只是在用户查找资源时判断是否有权限,那么位图的含义也非常明了,有查看权限的使用1表示,没有查看权限使用0表示。
那么我们可以简单计算一下,文章的权限相当于是文章的资源数据,那么我们的位图就可以是此公司下的所有人是否对于文章有访问权限。假设公司有10000个人,那么
总数位是 = 1 人/ 位 * 10000人 = 10000位
转换为字节 = 10000位 ÷8位/字节 = 1250字节 ≈ 1.221KB
也就是说,1.221KB的存储就可以代表此文章的所有访问的权限,可以将用户的唯一id进行hash计算出唯一的位置,即可得到位图。
但是,如何解决人员变动的问题呢?
动态位图
对于人员离职,或者变更的情况下,我们只需要将其位设置为0,并且将此位作废。可以做定期回收,或者废弃的位达到一定数量,然后重新进行hash运算,形成新的位图。
一般情况下,是否回收离职员工的位,取决于实际业务场景。比如我们需要做某些业务的权限审计,就需要保留对应的权限位。
新员工入职时,如果员工id使用字符串进行哈希运算的话,就可能会导致哈希冲突。所以可以给每个员工加上一个工号,使用int自增类型的工号进行哈希运算。
但即使有回收的机制,对于数据量很大的用户量,位图的维护将会非常的困难。所以我们暂不考虑使用位图的方式,这是因为对于我们的很多业务而言,可以跨租户访问,也就是说,我们的资源权限(比如的文档的查看权限)可以提供给其他公司人员访问,那么假设我们的用户量很大(百万级)那么这个位图的大小,以及频繁地扩容或者缩容,权限回收等内容,以及大多数的位都是0(非此文档所在的公司的员工)。对于这种情况,我们可以去维护一个映射表,但具体的实现也比较的复杂。
官方的解决方案
其实官方也在正在努力解决这个问题:
其实官方的思路也在向位图上面靠拢,我在OpenFGA的github项目中曾看到,OpenFGA的开发人员展示了其使用位图的一种思路,很遗憾的是当时没有留下笔记或截图,现在不知道在哪个地方可以找到。
这个问题,我之前看到其24年一月就打开了,一年了,到目前仍未解决,曾一度认为其难度短期内无法提供一个有效的解决方式。
但乐观的是,我在油管上面回看了OpenFGA今年一月份的会议,项目经理表示,目前正在推动 使用大型基数关系的权限简化搜索 这个问题。
短期内的做法
如果实在是觉得动态位图不好落地,或者短期内等不及官方提供的方案,那么我们可以抛弃掉掉这个前提。
也就是放弃掉权限系统和业务系统的数据分离,让业务系统也维护一部分权限内容。
比如我们前文提到的这样一个例子。
文章有一个协作者的概念,我们让业务系统,也就是文章增加一个协作者的存储。用户访问时,带上用户的所有信息(个人信息,所在公司,所在小组),也就是可以是协作者的所有类型(个人、小组、 公司),然后通过es即可直接查询到此用户可以访问的文章。
当然为了安全性,还可以在用户点击时,使用OpenFGA的check接口,二次验证一下用户是否真的有此文章的权限。
总结
针对于Search With Permissions问题,方案一和方案二对于可访问的数据量都有一定的局限性。方案三适合大多数场景,但是实际落地还是比较复杂的。
最简单的方式就是直接放弃业务数据和权限数据的分离,这样既可以提高查询的性能,又可以减少权限系统的复杂度,但是和我们的初衷有一定的相悖。
当然我也不会放弃探索,后续也会研究一下使用动态位图的落地方案。
未经本人允许,请勿转载。