第 6 / 21 篇

第5章 对象授权必须靠近业务数据

租户不是一个可以自由填写的过滤条件

多租户系统常把 tenant 放进请求 JSON 或路径,这方便路由,却不证明调用方拥有该租户的权利。正确顺序是先建立经过验证的主体与当前成员关系,再比较请求租户,最后在该租户范围内查找业务对象。不能先按全局 ID 找到对象,再只检查“用户已经登录”。

实验中的对象键由 tenant、kind 和 id 组合形成。Acme 的 o-100 和 Beta 的 o-200 属于不同边界。代理检查令牌租户与操作租户一致,业务服务重复这个检查,并从自己的对象存储读取对象。即便将来代理出现绕过,业务端仍然拒绝跨租户请求。授权规则和查询边界都应体现租户条件,不能只依赖前端把列表隐藏。

实际数据库查询可表达为 WHERE tenant_id = ? AND order_id = ?,并使用来自服务端身份上下文的 tenant 参数。ORM 自动过滤器能够减少遗漏,但应针对批量查询、原生 SQL、后台任务和管理员入口单独验证。对象缓存的键同样必须包含租户;否则数据库正确,缓存仍可能把 Beta 的对象返回给 Acme。

scope 只提供粗粒度上限

orders:refund 表达调用方具备发起退款类操作的资格,不能证明任意订单、任意金额和任意状态都允许退款。具体规则还包括订单属于当前租户、订单处于已支付状态、金额在允许范围内、审批绑定当前参数,以及主体尚未被撤权。scope 是上限,不是业务判断的替代品。

本书的 validOperation 限定订单 read/refund、工单 close、整数分金额与基本字段长度。业务端再使用 allowedScope 检查动作范围。订单状态必须是 paid 才能退款,工单必须 open 才能关闭。审批通过不会把非法状态变成合法状态;执行时应重新读取当前业务事实。

一个失败案例是:客服获得退款 scope,工具代理允许调用退款 API;服务只校验金额为正,没有验证订单租户。攻击者从浏览器日志看到别家订单号,修改参数后退款成功。修复应在服务端按当前租户查对象,并让测试从业务端直连攻击,而不是只测代理会拒绝。否则代理未来增加新路由时,漏洞又会出现。

当前关系比旧声明更重要

签名正确的 tenant 声明仍可能过时。员工调岗、组织拆分、管理员修正成员关系后,旧凭据不能永久决定归属。实验调用在线 /state,比较当前 tenant、epoch 与 active 状态。因此,即使测试私钥签出 Alice 属于 Beta 的凭据,也会因权威状态不一致被拒绝。这展示了签名和当前授权状态不同的职责。

生产系统不一定每次向身份平台查询租户。可以在业务授权库中保存成员关系、使用带版本的授权缓存,或针对高风险动作执行强一致查询。关键是明确一致性与撤权窗口,并保证敏感操作的最终判定读取足够新鲜的事实。把所有关系都压进长寿命 JWT 会让缓存失效变得困难。

可运行实验与边界

运行 go test -race -run 'TestTenantScopeAndUntrustedToolInput|TestTicketIsolationAndApproval' -v ./lab。测试同时从代理和业务接口提交跨租户订单,构造签名正确但成员关系错误的令牌,使用只读 scope 请求退款,并验证 Bob 能关闭 Beta 工单却不能操作 Acme 工单。每一项都应该得到明确拒绝,而不是由不存在的对象碰巧产生失败。

实验没有实现字段级权限、组织层级、共享对象或授权策略语言。在真实平台中,你可能需要 RBAC 管理岗位资格、ABAC 表达对象状态与金额、关系模型表达团队与客户归属。选择模型前先列业务不变量,不要因为某个策略引擎支持复杂表达式,就把所有规则从业务事务里搬走。

练习:从你的系统选三个敏感动作,为每个动作写出主体、租户、对象、状态、金额和时间条件。再为批量接口设计一个混合租户输入,说明应该整批拒绝还是逐项返回。无论选择哪一种,都必须避免部分越权成功。

列表、详情与批量接口要保持同一原则

对象授权容易在详情接口做对,却在搜索、导出和批量更新中遗漏。列表应在查询阶段按租户和可见范围过滤,不能先取全量结果再让模型隐藏其他租户。详情接口应再次验证,因为对象 ID 可以从历史日志或外部链接获得。导出通常包含更多字段,更不能因为是后台任务就默认拥有全局权限。

批量操作需要选择明确语义。若全有全无,必须在同一受控事务或计划提交模型中检查全部对象;若允许逐项结果,每一项都要独立授权,并避免用错误差异泄漏其他租户的对象存在性。审批摘要应覆盖最终对象集合,后续分页或重试不能悄悄加入新对象。

本书只提供单对象接口,正是为了让授权与状态迁移更容易观察。扩展时不应简单把 Operation 包成数组,然后循环调用而忽略部分成功。先定义混合租户、重复对象、对象中途变更和执行一半崩溃时的结果,再决定 API 形式。接口形状会影响授权能否被用户和执行器正确理解。