Files
qinglong/docs/adr/ADR-0028-project-identity-rbac-and-policy-core.md
T

9.1 KiB
Raw Blame History

ADR-0028Project Identity、版本化 RBAC 与默认拒绝 Policy Core

  • 状态:Proposed
  • 日期:2026-07-19
  • 关联:QL-RFC-0001、ADR-0025、ADR-0027

上下文

QingLong 2.x 只有面板单用户登录和 API App route scope。logs/crons/envs 等 scope 只决定某类 /open/* 路由能否进入,既没有稳定 ActorRef,也不绑定 Project、资源 identity 或审批策略。把 logs 直接解释为 3.0 artifact.read 会让一个旧 token 获得所有 Project 日志;把当前用户名直接作为 owner 又会把可修改的显示值误当成持久安全主体。

ADR-0027 已要求 Artifact 在任何文件访问前执行 artifact.read,但只定义了 authorizer port。3.0 还需要一个可在 edge 上低成本运行、在 cluster 中可替换存储 adapter、能够表达 User/API App/MCP Client/Agent/System/Worker 的基础 Policy Core。

决策

1. ActorRef 使用统一、精确且有界的主体

主体固定为:

interface PolicySubject {
  type: 'user' | 'api_app' | 'mcp_client' | 'agent' | 'system' | 'worker';
  id: string;
}

ID 为 1~255 字符,不允许控制字符。所有 subject、Project、RoleBinding、snapshot 和 policy request 都拒绝未知字段,避免调用方提交一个看似生效但实际被忽略的 permissions/context。Artifact read 已复用同一 subject 类型,不再维护第二套 identity 枚举。

Authentication adapter 负责把 session/token/mTLS credential 映射为稳定 subjectPolicyEngine 不读取 HTTP header、cookie、用户名或 legacy App scope。身份认证与授权仍是两个边界。

2. Project 是权限和归档状态的事实源

0017-project-policy 新增 Projects

  • id/name/slug/status/version
  • created_at_ms/updated_at_ms
  • slug 唯一,status 为 active|archived
  • migration 创建 RFC 要求的 default Projectbaseline 时间为 0,表示从 legacy 升级继承而非伪造创建时间。

baseline Project 故意不创建 owner binding。生产升级必须通过一次性、可审计且认证后的 bootstrap claim 建立首个 owner;禁止根据当前 username、默认密码、system App 或任意已有 token 静默授予。ADR-0029 已孵化默认不可达的 bootstrap core,但本机控制台/authentication adapter 与产品入口尚未实现,因此 migration 注册仍不代表 3.0 API 可用。

3. RoleBinding append-only、按主体版本化

ProjectRoleBindings 的 identity 是 (projectId,subjectType,subjectId,version)。每次赋权、变更角色或撤销都追加一行:

  • state 为 active|revokedactive 必须有 rolerevoked 不得携带 role
  • role 为 owner|admin|operator|viewer
  • mutationId 在 Project 内唯一,同请求重放返回 existing,内容漂移返回稳定 mutation conflict
  • 调用方提供 expected current version,新版本必须严格 +1SQLite adapter 使用短 IMMEDIATE transaction,使并发赋权只有一个版本获胜;
  • 每行保存 changedBy ActorRef 和时间,不覆盖历史授权事实。

撤销是一条新版本,不删除旧行。当前读取使用 (project,subject,version DESC) 点查;另有 subject→Project 索引供未来列出可见 Project。edge 不把所有 membership 加载到内存,也不常驻缓存;cluster-control 必须实现 PostgreSQL CAS/locking adapter,不能共享 SQLite。

4. 首版角色权限矩阵固定在领域代码

静态权限包括 RFC 原有权限,并补充 project.managepolicy.manageapproval.decide。Tool 权限只接受精确 tool.call:{toolName};外部请求不接受 tool.call:*

Role 允许范围
owner Project 内全部已声明权限和精确 Tool call
admin project.manage 外的全部已声明权限和精确 Tool call
operator 读、Task create/update、Run start/stop/retry、Secret use、精确 Tool call
viewer project/task/run/artifact 只读

首版不引入用户自定义 role、外部策略语言或 allow/deny JSON。添加权限必须修改 canonical permission registry、矩阵和测试,不能依赖数据库自由字符串。接口保持独立,后续可接 OPA 或 Project custom policy,但新实现必须保持默认拒绝和相同 decision contract。

5. Policy decision 默认拒绝,Agent 写操作要求审批

稳定 decision effect 为 allow|deny|require_approvalreason 使用低敏机器码:

  • Project 不存在:deny/project_not_found
  • 无 binding 或已撤销:deny/subject_unbound
  • role 不包含权限:deny/permission_missing
  • archived Project 对非只读操作:deny/project_archived
  • role 允许:allow/role_grant
  • Agent 的写操作、Secret use/manage、管理权限和 Tool callrequire_approval/agent_action_requires_approval

Policy storage 损坏、snapshot identity 漂移或读取失败不返回 deny 原因,而返回稳定 unavailable 并 fail closed。因为当前 ApprovalRequest 状态机尚未实现,require_approval 不能被视为 allow;调用端不得先执行再补审批。

archived Project 只允许 project.read/task.read/run.read/artifact.read,方便历史审计;不允许启动 Run、使用 Secret 或管理 policy。

6. 旧身份不自动继承 3.0 权限

  • 2.x 面板登录只证明 legacy sessionADR-0030 已提供默认不可达的稳定 user/usr_legacy_primary 映射 core,但生产 middleware 尚未装配;
  • legacy API App 的 logs scope 不等于 artifact.readcrons 不等于 run.start
  • system App、Worker token、MCP session 和 Agent identity 都必须经各自 authentication adapter 产生 subject,并显式绑定 Project role
  • migration 不复制 client secret、token、username 或 password 到 RoleBinding。

未来兼容向导可以展示建议映射,但必须由已认证 owner 明确确认并写入审计 mutation。

7. 当前保持 production unreachable

本切片已实现 domain registry、ProjectPolicyEngine、SQLite append/resolve repository、0017 migration、schema ownership 和 ProjectPolicyArtifactReadAuthorizer;ADR-0029 另已实现默认不可达的首 owner bootstrap core。但仍没有:

  • 首 owner bootstrap 的本机控制台 issuer、认证 adapter、产品入口与恢复码;
  • session/token/mTLS → subject authentication adapter
  • role 管理 API/UI、审计事件和 rate limit
  • ApprovalRequest 持久化与一次性消费;
  • PostgreSQL repository、跨副本 cache invalidation 或 OPA adapter
  • typedi/Express/MCP/Worker startup 装配。

因此 default Project 在现有生产入口中仍是 ownerless,所有真实调用默认拒绝。不得为“让 API 能用”而在 loader 中隐式授予 owner。

影响

正面影响:

  • Artifact、Secret、Run、Tool 可以共享一套 Project/Actor permission vocabulary
  • 权限变更有不可覆盖历史、幂等 mutation 和 CAS 并发语义;
  • edge 每次决策只需有界点查,不需要策略 sidecar 或全量 membership cache
  • legacy token 不会因升级静默扩大权限;
  • Agent 写行为在 Approval 状态机完成前无法被误当成 allow。

代价与风险:

  • 初次升级必须完成显式 owner bootstrap,否则 3.0 API 保持不可用;
  • 每次无缓存决策至少一次数据库读取,未来缓存必须以 binding/project version 正确失效;
  • 固定角色不支持复杂 ABAC、资源标签和 Tool 风险等级;
  • 当前所有 Agent Tool call 都要求审批,未来只能由可信 Tool manifest 风险分类放宽,不能由 Agent 自报只读;
  • RoleBinding 历史持续增长,需要与 Project 生命周期一致的审计 retention,而不能直接覆盖或清空。

未选择的方案

  1. 把 legacy scope 直接映射为 Permission:没有 Project/资源边界且会扩大旧 token 权限,拒绝。
  2. 默认用户名自动成为 owner:显示名可变且没有一次性 bootstrap 审计,拒绝。
  3. RoleBinding 单行覆盖:丢失授权历史和并发裁决证据,拒绝。
  4. 把 permissions JSON 存进 binding:无法维护 canonical 语义,损坏/拼写会形成隐式权限,首版拒绝。
  5. 未绑定主体默认 viewer:跨 Project 数据泄露,拒绝。
  6. Agent role 允许即直接执行写操作:绕过 RFC 默认审批原则,拒绝。
  7. edge 引入 OPA sidecar:资源与部署复杂度不适合基础能力,首版拒绝。

验证要求

  • migration 在 legacy fixture 上创建 ownerless default Project,重复执行和 ownership audit 通过;
  • subject、permission、Project、binding、snapshot、request 非 canonical 输入在 repository/policy 副作用前拒绝;
  • role matrix、archived Project、unbound/revoked/default-deny 和 Agent approval 全覆盖;
  • append replay、mutation drift、stale expected version 和两个 SQLite 连接并发赋权可重复验证;
  • current row 损坏、active-null-role、identity 漂移和非 SQLite adapter fail closed
  • Artifact authorizer 只委托 artifact.read 且错误 action 拒绝;
  • Node 22/24 全量测试、类型检查、schema audit 和 GitNexus reachability 通过;
  • app/loaders/api/services 不得导入或装配本切片。