Files
qinglong/docs/adr/ADR-0047-postgresql-project-policy-and-role-binding-fence.md
T

6.5 KiB
Raw Blame History

ADR-0047: PostgreSQL Project Policy 与 RoleBinding Fence

  • 状态:Proposed
  • 日期:2026-07-19
  • 关联 RFCQL-RFC-0001 D-27 至 D-32、D-34 至 D-44、D-45

上下文

ADR-0046 已强制 /api/v3 在读取 body 前完成 Authentication、Policy 与安全审计,但当时 cluster-control 只有 Policy port,没有 PostgreSQL 权威实现。若直接注入 allow-all stub、复用进程内 role Map,或 deep-import legacy Sequelize adapter,都会让 readiness 已通过的多副本控制面在授权事实上一致性失效。

既有 SQLite 0017-project-policy 已定义 ownerless default Project、append-only versioned RoleBinding、六类 subject、固定 role matrix、mutation replay 与 expected-version CAS。PostgreSQL 必须保持相同领域结果,但不能逐字复制 SQLite 较弱的数据库约束或单写者事务假设。

决策

1. Profile-neutral Policy contract 进入 runtime-core

@qinglong/runtime-core/project-policy 冻结:

  • user / api_app / mcp_client / agent / system / worker 六类 subject
  • Project status、owner/admin/operator/viewer role 与 permission vocabulary
  • Project、RoleBinding、snapshot 与 append command exact-shape normalizer
  • ProjectPolicyRepository port
  • role matrix、archived read-only、Agent require_approval 和 Project/Binding version fence。

legacy 与 runtime-core 由 parity test 比较 vocabulary、effect、reason 和 fence。过渡期可以存在实现副本,但不得产生两套 Policy 语义。

2. pg-0004-project-policy 推进 capability v3

reviewed migration 新增:

  • ownerless ql3.projects,只创建无隐式 owner 的 default baseline
  • ql3.project_role_bindings,复合主键包含 Project/subject/version
  • Project 内 mutation 唯一索引、current lookup 与 subject lookup 索引;
  • active 必须携带有效 rolerevoked 必须为 role IS NULL
  • Project timestamp、subject、mutation 和版本使用数据库 CHECK
  • capability 从精确 v2 predecessor 推进为: {"project_policy":1,"run_core":1,"run_retry_policy":1}

Drizzle metadata、schema contract、migration checksum 和真实 catalog 必须 lockstep。readiness 看到 capability/history/table/index/CHECK/FK 任一漂移都保持 not-ready。

3. Runtime role 只能 append RoleBinding

精确 runtime privilege 为:

  • projects: SELECT、INSERT、UPDATE,禁止 DELETE
  • project_role_bindings: SELECT、INSERT,禁止 UPDATE、DELETE
  • schema CREATE 继续禁止。

Role revoke 通过 append 新 version 表达,禁止覆盖或删除旧授权事实。migration role 仍是唯一 DDL authority。

4. PostgreSQL append 使用数据库 fence

PostgresProjectPolicyRepository.append() 固定:

  1. SERIALIZABLE transaction 与硬 statement/lock/idle timeout
  2. SELECT Project ... FOR UPDATE,同时证明 Project 存在并串行化该 Project 的 Policy mutation
  3. 先按 (project_id, mutation_id) 检查精确 replay;不同 payload 使用同 mutation ID 时冲突;
  4. 读取 subject 最新 version,并与 expectedCurrentVersion 精确比较;
  5. 只 INSERT version N+1serialization/deadlock/lock conflict 有界重试;
  6. 行畸形、重复 identity、未知 SQL 错误与连接失败统一映射为低敏 unavailable。

两个并发 expected-version writer 只能有一个 winner。winner 的精确 replay 返回原事实,loser 不得通过最后写覆盖改变 role。

5. Readiness 后同时装配 Run 与 Policy Repository

cluster-control 只有在 capability v3、catalog 和 runtime role 全部通过后,才向 application factory 提供同一 Pool 上的 RunRepositoryProjectPolicyRepository。admission 使用共享 ProjectPolicyEngine adapter 产生 allow/deny/approval 与 fence;无 Project scope 的 operation 默认 deny。

被否决的替代方案

复用 legacy Sequelize Policy adapter

拒绝。该 adapter 明确 SQLite-only,并会让 cluster artifact 反向加载 legacy root、Sequelize 与本地数据库假设。

用 upsert 保存 subject 当前 role

拒绝。会丢失撤权历史,使旧 approval/fence 无法解释,也无法区分精确 replay 与覆盖写。

只靠唯一索引竞争,不锁 Project

拒绝。mutation identity 与 subject version 是两个约束域,仅处理 23505 不能证明 Project 存在检查、replay 和 version winner 在同一快照内成立。

缓存 role 并异步失效

当前拒绝。未定义可靠 version invalidation 前,缓存会让 revoke 后的旧 allow 在其他 pod 继续生效。首版先走有界数据库点查。

影响

正向

  • cluster-control 不再需要 allow-all Policy seam
  • SQLite/PostgreSQL 对相同 snapshot 返回相同 effect/reason/fence
  • RoleBinding 为 append-only durable fact,支持审计、撤权和并发裁决;
  • runtime role 即使被业务 bug 驱动,也不能 UPDATE/DELETE binding history
  • Project row lock 将同一 Project 的低频权限 mutation 与高频 Policy read 分离,不阻塞普通 read。

代价

  • 同一 Project 的 RoleBinding mutation 被串行化;这是权限管理低频路径的有意选择;
  • runtime role 对 Project 需要 UPDATE 权限以执行 FOR UPDATE 和后续 Project 管理,必须继续由 Repository/Policy 限制;
  • 当前 ADR 只完成 Project/RoleBinding core;后续 ADR-0048/0049 已补齐受审 route registry、cluster credential authentication 与持久化安全审计,Project/Role/credential 管理 API、audit retention/query 和 Approval PostgreSQL tables 仍未完成;
  • runs.project_idprojects.id 的跨历史 backfill/FK 尚未决策,不能在已有 alpha 数据上盲加约束。

验证

  1. runtime-core normalizer/engine unit test覆盖 exact shape、role matrix、archived、Agent approval 和 fail-closed。
  2. legacy/runtime parity test覆盖 vocabulary、effect、reason 与 fence。
  3. migration checksum、capability predecessor、Drizzle/schema/catalog lockstep test全部通过。
  4. fake Pool test覆盖 resolve、SERIALIZABLE 顺序、exact replay、version/mutation conflict、rollback/retry 和 corrupt row。
  5. 真实 PostgreSQL 双连接测试证明同 expected version 只有一个 winnerwinner 可精确 replay。
  6. 真实数据库直接 INSERT 证明 revoked+role 与逆序 timestamp 被 CHECK 拒绝。
  7. 独立 runtime role 证明 Project 无 DELETE、RoleBinding 无 UPDATE/DELETE、schema 无 CREATE。
  8. PostgreSQL 16/18 × x64/arm64 CI 继续作为正式 server/readiness 证据;较旧 PostgreSQL 必须被 production readiness 拒绝。