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

141 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 使用统一、精确且有界的主体
主体固定为:
```ts
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|revoked`active 必须有 rolerevoked 不得携带 role
- role 为 `owner|admin|operator|viewer`
- `mutationId` 在 Project 内唯一,同请求重放返回 existing,内容漂移返回稳定 mutation conflict
- 调用方提供 expected current version,新版本必须严格 `+1`SQLite adapter 使用短 `IMMEDIATE` transaction,使并发赋权只有一个版本获胜;
- 每行保存 `changedBy` ActorRef 和时间,不覆盖历史授权事实。
撤销是一条新版本,不删除旧行。当前读取使用 `(project,subject,version DESC)` 点查;另有 subject→Project 索引供未来列出可见 Project。edge 不把所有 membership 加载到内存,也不常驻缓存;cluster-control 必须实现 PostgreSQL CAS/locking adapter,不能共享 SQLite。
### 4. 首版角色权限矩阵固定在领域代码
静态权限包括 RFC 原有权限,并补充 `project.manage``policy.manage``approval.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_approval`reason 使用低敏机器码:
- 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 call`require_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.read``crons` 不等于 `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 不得导入或装配本切片。