模块 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), 且应用角色是非超级用户,并且从不具备BYPASSRLS;FORCE让策略连表的属主 也一并受约束。属主身份是一项部署选择:默认的单角色安装会让应用角色成为 数据库属主——RLS 依然约束它,但属主可以修改自己的表,因此这种姿态是 可察觉篡改(tamper-evident),而非属主无法绕过(owner-proof)。 真正的硬性权限边界——应用角色同时也是非属主——来自 owner/app 分离的拓扑: 由一个独立的 owner 角色执行 provisioning,应用角色只获得它所需的 DML。 - 在 SQLite(单节点部署)上没有行级安全;其等效性来自两个事实—— 通向数据库的唯一路径是描述符生成的 SQL,它总会追加租户谓词, 并且**绊线触发器(tripwire triggers)**会中止任何其租户与固定作用域不匹配的写入。
一次启动自检会在迁移之后查询活动中的隔离守卫,
若任何携带 tenant_id 的表未受保护,则拒绝打开存储层——
因此一张新表上被遗漏的守卫会变成一次启动失败,而非一次静默的泄漏。
它消费什么、产出什么
Section titled “它消费什么、产出什么”模块 XX 没有事件总线接口,也没有任何执行(actuation)。
它不消费 edge.observed,不发出 findings,也不调用任何 provider——
它是其他模块借以写入的基底。它唯一可观测的效果是结构性的:
任何模块持久化的每个实体都已经是租户作用域内的,
且对一个被审计实体的每次变更都会在同一事务内追加到该租户的
哈希链式审计账本(hash-chained audit ledger)。