通过逆向思维解决细粒度授权问题
内容
FGA基础知识### FGA作为增量计算问题### 用SQL构建增量FGA引擎### 没有免费的午餐!### 实现动态FGA模型### 总结
如果你一直在关注安全领域,你可能会注意到**细粒度授权(FGA)**访问控制模型的兴起。访问控制模型决定了谁可以访问应用程序以及他们可以执行哪些操作。这些模型通常需要根据应用程序的特定需求进行定制。FGA提供了一种原则性的方法来构建特定于应用程序的访问控制模型,同时将授权逻辑与应用程序逻辑分离。通过FGA,开发者可以使用声明性语言编写特定于应用程序的访问控制规则。FGA引擎在运行时动态执行这些规则。
FGA最初由Google的Zanzibar系统推广,现在有许多实现,包括开源和商业解决方案。
像任何强大的抽象一样,FGA的实现可能代价高昂。任何FGA平台的核心都是一个计算引擎,它实时评估FGA规则。在一个大型系统中,这个引擎必须在包含数百万个对象的对象图上每秒评估数千个请求。每个授权请求都可能需要进行昂贵的图遍历。

如何在不使云账单飙升的情况下构建这样的系统?在这篇博客中,我们展示了如何通过逆向思维实现这一目标。我们不是从头开始评估每个单独的授权请求,而是预先计算所有相关的授权决策,将授权请求转化为简单的键/值查找。
为了实现这一点,我们需要一种方法来保持预计算结果的更新。对象图不断变化,可能会使之前计算的授权决策失效。为了处理这些变化,我们需要一个计算层,能够增量更新其输出,而无需完全重新计算。为此,我们使用了Feldera,我们的增量SQL引擎。Feldera支持任意SQL查询的增量评估,包括相互递归的查询——这是在SQL中实现迭代图遍历的关键特性。

在本文的其余部分,我们将介绍FGA的基础知识,并使用几行SQL实现一个高性能的FGA引擎。
FGA基础知识
对象图
FGA策略定义在一个对象图上,其中节点表示系统对象,边捕获这些对象之间的关系。以一个简单的文件管理服务为例。该应用程序处理三种类型的对象:users、user groups和files(为简单起见,我们使用单一对象类型对文件和文件夹进行建模),它们通过以下关系连接:
member- 用户与组之间的关系。parent- 文件之间的关系,定义了文件夹层次结构。editor- 组与文件之间的关系,赋予组读取或写入文件的权限。viewer- 组与文件之间的关系,赋予组读取文件的权限。
我们使用relationship(object1, object2)表示法来表示关系,例如member(alice, engineering)、parent(folder1, file1)、editor(admins, folder1)等。以下是一个示例对象图,显示了几个用户、组和文件。请注意,对象可以具有属性,例如is_banned。

规则
FGA策略由从对象图派生新关系的规则组成。规则由前提条件和派生子句组成,前提条件必须满足才能触发规则,派生子句在前提条件满足时成立。
在我们的文件管理器示例中,我们定义了以下派生关系:
group-can-read(group, file)-group可以读取file。group-can-write(group, file)-group可以写入file。user-can-read(user, file)-user可以读取file。user-can-write(user, file)-user可以写入file。
这些关系由以下规则管理:
- 规则1:
editor(group, file) -> group-can-write(group, file)- 如果组是文件的编辑者,它可以写入该文件。 - 规则2:
group-can-write(group, file1) and parent(file1, file2) -> group-can-write(group, file2)- 如果组可以写入文件,那么它可以写入其任何子文件。 - 规则3:
viewer(group, file) -> group-can-read(group, file)- 如果组是文件的查看者,那么它可以读取该文件。 - 规则4:
group-can-write(group, file) -> group-can-read(group, file)- 对文件的写入权限意味着对同一文件的读取权限。 - 规则5:
group-can-read(group, file1) and parent(file1, file2) -> group-can-read(group, file2)- 如果组可以读取文件,那么它可以读取其任何子文件。 - 规则6:
member(user, group) and group-can-write(group, file) and (not user.is_banned) -> user-can-write(user, file)- 如果用户是可以写入文件的组的成员,并且用户未被禁止,那么用户可以写入该文件。 - 规则7:
member(user, group) and group-can-read(group, file) and (not user.is_banned) -> user-can-read(user, file)- 如果用户是可以读取文件的组的成员,并且用户未被禁止,那么用户可以读取该文件。
根据这些规则,我们可以从上面的示例对象图中派生出对文件f1的以下访问权限:
user-can-write(emily, f1)user-can-read(emily, f1)user-can-write(irene, f1)user-can-read(irene, f1)
FGA作为增量计算问题
评估授权规则是一项昂贵的操作。每当用户尝试对资源执行操作时,FGA引擎必须检查对象图中是否存在一条从用户到资源的路径,该路径符合授权规则。例如,上图中的红色路径显示了user-can-write(emily, f1)关系的派生。在图中查找路径的最坏情况复杂度与图的大小(节点数 + 边数)成正比。
而这只是一个授权检查!在一个大规模应用程序中,系统可能需要在每秒处理数千个这样的检查,每个检查的延迟预算为几十毫秒。
增量计算为这一挑战提供了一个优雅的解决方案。 不是为每个请求从头开始评估授权决策,而是预先计算所有授权决策并将其存储在键值存储中,将运行时授权检查简化为简单高效的查找。随着对象图的演变,增量计算确保只有受变化影响的规则派生被更新,避免了完全重新计算。一个高效的增量查询引擎(如Feldera)可以在对象图变化后的几毫秒内更新计算,即使对于非常大的图也是如此,确保运行时授权检查反映系统的当前状态。
用SQL构建增量FGA引擎
是时候写一些SQL了!让我们使用Feldera实现文件管理器授权模型。这里描述的实现可以作为Feldera在线沙箱中的预打包示例以及本地Feldera安装中的示例。
建模对象图
我们从将三种对象类型——用户、组和文件——建模为SQL表开始:
sql
createtable users (
id bigintnotnullprimary key,
name string,
is_banned bool
) with ('materialized'='true');
createtablegroups (
id bigintnotnullprimary key,
name string
) with ('materialized'='true');
createtable files (
id bigintnotnullprimary key,
name string,
-- 父文件夹id;根文件夹为NULL。 parent_id bigint) with ('materialized'='true');
请注意,parent_id字段建模了文件之间的parent关系。
接下来,我们建模member、editor和viewer关系:
sql
-- Member关系建模用户在组中的成员资格。createtable members (
id bigintnotnullprimary key,
user_id bigintnotnull,
group_id bigintnotnull) with ('materialized'='true');
-- Editor关系赋予组读取或写入文件的权限。createtable group_file_editor (
group_id bigintnotnull,
file_id bigintnotnull) with ('materialized'='true');
-- Viewer关系赋予组读取文件的权限。createtable group_file_viewer (
group_id bigintnotnull,
file_id bigintnotnull) with ('materialized'='true');
实现规则
我们现在准备实现派生关系。我们从由以下两条规则定义的group-can-write关系开始。
- 规则1:
editor(group, file) -> group-can-write(group, file)。 - 规则2:
group-can-write(group, file1) and parent(file1, file2) -> group-can-write(group, file2)。
在规则2中,group-can-write出现在蕴含的两边,表明这是一个递归关系。规则1指定了递归的基本情况:组对其作为编辑者的所有文件具有写权限。规则2定义了递归步骤:写权限从文件传播到其所有子文件。我们将这些规则实现为递归SQL视图:
sql
declarerecursiveview group_can_write (
group_id bigintnotnull,
file_id bigintnotnull);
create materialized view group_can_write as-- 规则1: editor(group, file) -> group-can-write(group, file).(
select group_id, file_id from group_file_editor
)
unionall-- 规则2: group-can-write(group, file1) and parent(file1, file2) -> group-can-write(group, file2).(
select group_can_write.group_id,
files.id as file_id
from group_can_write join files on group_can_write.file_id = files.parent_id
);
请注意,在编写这些查询时,我们不需要担心增量评估。作为开发者,你只需指定如何从现有关系派生新关系。引擎会自动处理增量计算。
我们继续以类似的方式实现规则3到7。请参阅我们的FGA教程获取完整代码。
测试
将文件管理器示例的完整SQL代码复制到Feldera Web控制台。启动管道并通过发出以下临时查询填充对象图以匹配上述示例:
sql
insertinto users values (1, 'emily', false),
(2, 'irene', false),
(3, 'adam', true);
insertintogroupsvalues (1, 'engineering'),
(2, 'it'),
(3, 'accounting');
insertinto files values (1, 'designs', NULL),
(2, 'financials', NULL),
(3, 'f1', 1),
(4, 'f2', 1),
(5, 'f3', 2);
insertinto members values (1, 1, 1), -- emily在engineering组 (2, 2, 2), -- irene在IT组 (3, 3, 3); -- adam在accounting组
insertinto group_file_editor values (1, 1), -- 'engineering'可以编辑'designs' (2, 1), (2, 2), -- 'it'可以编辑'designs'和'financials' (3, 2); -- 'accounting'可以编辑'financials'.
insertinto group_file_viewer values (3, 1); -- 'accounting'可以查看'designs'.
我们现在可以验证程序的输出,例如:
sql
select users.name as user_name,
files.name as file_name
from user_can_read
join users on users.id = user_can_read.user_id
join files on files.id = user_can_read.file_id;
| user_name | file_name |
|---|---|
| emily | designs |
| irene | designs |
| irene | financials |
| irene | f3 |
| emily | f1 |
| irene | f1 |
| emily | f2 |
| irene | f2 |
正如预期的那样,emily作为engineering组的成员,对designs文件夹下的所有文件具有读取权限,而irene作为it组的成员,可以读取designs和financials下的文件。 |
|
接下来,我们对对象图进行增量更改,将emily添加到it组: |
|
| sql |
insertinto members values (4, 1, 2);
运行上述select查询将返回两行额外的结果:
全速运行
Feldera沙箱提供了上述程序,配置了一个数据生成器,该生成器构建了一个包含1,000个用户、100个组、100个顶级文件夹、1,000个子文件夹和100,000个文件的随机对象图,这些文件随机分布在子文件夹中。生成器持续运行,动态更新100,000个文件的随机集合。此外,它不断修改用户组成员资格。
在配备M3 Max CPU的MacBook Pro上运行,该程序实现了115K更新/秒的持续吞吐量,意味着它每秒处理115K对象图变化并更新所有派生关系。
请注意,沙箱中的数据生成器配备了速率限制器,将吞吐量限制为每秒2,000个事件,以节省共享沙箱资源。
没有免费的午餐!
增量计算带来了巨大的性能提升:不是为每个授权请求执行昂贵的图遍历,而是所有计算都在对象图变化时预先完成。引擎通过计算派生关系的更新来处理这些变化,时间与更新的大小成正比。
这些改进的代价是增量查询引擎用于执行其工作的额外存储。具体来说,它必须存储从对象图派生的所有关系,这可能需要与图中节点数成二次方的存储空间。作为一个具体示例,在一个包含1M用户和1M公共文件的系统中,策略引擎可能会派生出10^12个user-can-read关系。
因此,一个可扩展的FGA系统必须小心地只派生系统执行的授权检查可以访问的关系子集。例如,只有登录系统的用户才能发出授权请求;因此我们只需要在计算中考虑当前活跃的用户。请参阅我们的详细教程以获取更详细的讨论和此类优化的具体示例。
实现动态FGA模型
如果我们不仅希望对象图,还希望访问控制模型本身在运行时发生变化,该怎么办?例如,考虑构建一个像OpenFGA这样的通用FGA平台,用户可以定义新的对象类型、关系和规则。在文件管理器示例中,我们可能希望添加一种新的关系类型owner,它赋予用户永久删除系统中文件的权限。
事实证明,这也可以表示为增量SQL程序。关键思想是将FGA规则建模为关系数据,而不是将其硬编码为SQL查询。请参阅我们的完整教程以获取详细信息。
总结
关注点分离是计算机科学家完成任务的方式。在本文中,我们隔离了高效评估FGA规则的问题,并将其重新定义为增量计算问题。我们使用了一个专门的增量计算工具来解决这个问题,以最小的开发工作量实现了出色的单节点性能:我们只需编写几行简单的SQL查询。
无论你是在为应用程序添加灵活的授权层,还是构建通用策略框架,你都可以考虑使用像Feldera这样的增量查询引擎作为计算引擎。Feldera的两个特性使其非常适合这一目的:
- 增量查询评估使Feldera能够通过避免完全重新计算来实时处理对象图或FGA模型的变化。
- 支持相互递归视图允许通过递归查询自然地捕获迭代图遍历。
