跳转到内容

模块 XX — 多租户与组织管理

模块 XX 并不是一个挂接在引擎之外的服务——它是引擎本身的一项属性。 没有一个独立的租户模块可供附加;相反,核心数据模型在每个实体上携带租户边界, 存储层在每次查询之下强制执行它。本页是该边界今天所保证内容的参考, 也涵盖组织管理中仍处于设计阶段的部分。

多租户位于引擎层(第 0 层),与平台自身的 API(模块 XIX)并列, 因为在一个运行中的数据模型上事后改装隔离,正是那种之后无法安全完成的变更。 每个核心实体都携带一个 tenant_id,调用方绝不会将其作为自由参数传入: 它一次性固定租户,并获得一个其仓储已经绑定到该租户的作用域。 API 中没有跨越租户的词汇——在任何数据库机制之前,这种缺失就是第一道隔离屏障。 特权的跨租户作用域(创建组织、列出组织、删除一个租户)仅可由引擎自身的启动过程触及, 模块永远无法触及。

租户模型由数据模型契约拥有,而非由某个按模块划分的 schema 拥有。 根实体是 Org,它就是租户:当引擎播种一个组织时, 其标识符即成为租户标识符,并在同一时刻确立该组织自身的审计链。 其他每个核心实体——agent、会话、资源、身份、策略、成本记录、findings、部署、 访问地图(access map)以及审计账本(audit ledger)——都是在某个租户作用域内部创建的, 并在写入时打上该租户的标记;调用方无法覆盖它。

隔离在查询层强制执行,因部署方式而异:

  • PostgreSQL 上,每张携带 tenant_id 的表都在 FORCE ROW LEVEL SECURITY 下运行,并按事务绑定一条 tenant_isolation 策略。 未能绑定租户的事务会抛出错误,而不是静默地返回零行(fail-closed), 且应用角色是非超级用户,并且从不具备 BYPASSRLSFORCE 让策略连表的属主 也一并受约束。属主身份是一项部署选择:默认的单角色安装会让应用角色成为 数据库属主——RLS 依然约束它,但属主可以修改自己的表,因此这种姿态是 可察觉篡改(tamper-evident),而非属主无法绕过(owner-proof)。 真正的硬性权限边界——应用角色同时也是非属主——来自 owner/app 分离的拓扑: 由一个独立的 owner 角色执行 provisioning,应用角色只获得它所需的 DML。
  • SQLite(单节点部署)上没有行级安全;其等效性来自两个事实—— 通向数据库的唯一路径是描述符生成的 SQL,它总会追加租户谓词, 并且**绊线触发器(tripwire triggers)**会中止任何其租户与固定作用域不匹配的写入。

一次启动自检会在迁移之后查询活动中的隔离守卫, 若任何携带 tenant_id 的表未受保护,则拒绝打开存储层—— 因此一张新表上被遗漏的守卫会变成一次启动失败,而非一次静默的泄漏。

模块 XX 没有事件总线接口,也没有任何执行(actuation)。 它不消费 edge.observed,不发出 findings,也不调用任何 provider—— 它是其他模块借以写入的基底。它唯一可观测的效果是结构性的: 任何模块持久化的每个实体都已经是租户作用域内的, 且对一个被审计实体的每次变更都会在同一事务内追加到该租户的 哈希链式审计账本(hash-chained audit ledger)