系统设计 / 访问控制 · 从 RBAC 查表到 policy 判定函数 / ReBAC:权限是 user 与 object 之间的一条边 待审核 17 / 23
ReBAC · 关系即权限

ReBAC:权限是 user 与 object 之间的一条边

what:resource 与属性 留了一句话没有展开:owner 是关系,不是属性的一个取值。把这句话当真,授权模型随即换了形状:规则不再读某个字段的值,而是问这两个点之间有没有一条边。这条路线叫 ReBAC,Google Drive 的共享、GitHub 的仓库协作、Notion 的页面继承都走它。

1 · relation tuple 与向上的传递

一条 relation tuple 有三段:object、relation、user,写作 doc:季度汇报#viewer@alice。文件夹与文档之间的层级同样是 tuple,写作 doc:季度汇报#parent@folder:A1,「授权」与「层级」因此共用同一张图、同一种记法。判定 check(user, relation, object) 也就不再是查表,而是从 object 出发沿边搜索:先看有没有直接命中的 tuple,没有就顺 parent 上一跳,直到根节点或命中为止。

图 1-1 · 15 个节点的文件夹树上,一次 check 沿 parent 链向上的搜索路径。可切换访问者与目标文档、单击节点改挂 tuple,或把文件夹 A1 移到另一个部门下。

2 · 省下来的不是条数

为什么:在 User 和 Permission 之间插一层 Role 算过一笔连线账:ACL 的 M×NM \times N 被 Role 降到 M+NM + N。ReBAC 看起来该有同样的算术,实际算下来不成立:图 1-1 那棵树上,关系侧共 18 条 tuple(14 条 parent 加 4 条成员),而逐 (user, doc) 展开的 ACL 只有 15 行,tuple 反而更多。

真正的差别在变更的代价。给一个人授权整个 Acme 是 1 条 tuple,展开成 ACL 是 8 行;把文件夹 A1 从部门 A 挪到部门 B,关系侧改写 1 条 parent,ACL 侧则要为每个用户重算 A1 子树下的所有行。ReBAC 的比值是「一次写入覆盖多少对象」,不是「总共存了多少条」。

注 · 本节原本打算复用 M+NM + N 那套论证,是 15 : 18 这两个数把它推翻的:节点数够小时,parent 边的固定开销压不下去。tuple 数与 ACL 行数的交叉点取决于树的形状:文档越多、授权点越靠上,关系侧越划算;一人一文档地精确授权,ReBAC 就退化回 ACL,还多存了一份树结构。

3 · userset rewrite:关系之间的蕴含

viewer 不必逐条写全。Zanzibar 把关系之间的推导写进 namespace 配置,称为 userset rewriteeditor 蕴含 viewer(能编辑的当然能看),folderviewer 顺着 parent 蕴含其中每份 docviewer(论文里的 tuple_to_userset 算子)。配置由 union、intersection、exclusion 三个算子组合而成,与 决策与组合算法 的裁决逻辑不是一回事:那里是多条规则的冲突收敛,此处是一条关系向另一条关系的展开。

4 · check 的方向与一致性

同一个判定有两条走法。从 object 向上找,代价随树的深度增长,适合层级深、授权点少的形状;从 user 向下展开他所有的 userset,代价随授权面增长,Zanzibar 的 Expand 接口暴露的就是这一侧。真实系统按形状二选一,或者两侧各建索引。

关系图分布在多地副本上,于是「撤销之后还会不会被放行」变成一个一致性问题。论文管它叫 new enemy problem:先把某人从共享名单里移除、再往文档里写入敏感内容,若判定读到的是移除之前的快照,被移除的人照样能读到新内容。Zanzibar 的答案是 zookie,一个随内容一起存下的一致性令牌,判定时要求快照不早于它。

警示 · 撤销的延迟在 ReBAC 里比在 RBAC 里更容易被忽视:删掉一条 tuple 只是删掉一条边,而这条边的影响面是它下游的整棵子树,缓存里可能已经躺着成百上千条由它推导出的判定结果。缓存与失效的账见 数据层 §4。

5 · 与 ABAC 的分界

本系列 policy.ts 的 8 条规则里只有 r-owner 读关系,而且只读一跳:resource.owner === subject.id。一跳的关系用属性比较就能写完,这也是 ABAC 能覆盖大半场景的原因。两跳以上就不行了——「我是这份文档所属文件夹所属部门的负责人」要用属性表达,只能把整条路径预先算好塞进 subject 或 resource 的属性里,而每次层级调整都要重算这份冗余。

建议 · 两者按维度分工:对象之间的层级与共享关系交给 ReBAC,环境与阈值类条件(工作时间、密级比较、设备状态)留给 ABAC。Zanzibar 本身不读时间也不读设备,真实系统里它前面通常还有一层条件判定;反过来,只有 ABAC 而没有关系模型,共享与继承就得靠业务代码手写递归。

6 · 参考文献

  1. Pang, R., Cáceres, R., Burrows, M., et al. (2019). Zanzibar: Google's Consistent, Global Authorization System. USENIX ATC '19. tuple 记法、userset rewrite、zookie 与 new enemy problem 的出处。research.google
  2. OpenFGA. Modeling Guide. Zanzibar 模型的开源实现,把 namespace 配置写成可读的 DSL。openfga.dev
  3. 本系列。what:resource 与属性owner 那条规则如何同时属于 ABAC 与 ReBAC 两侧。