Skip to content

plugin-approvals 的 admin 豁免读 session.roles,而 ObjectQL 的 buildSession() 从不填充它 —— 记录锁/delegation 守卫的 admin 覆盖在真实引擎路径上永不生效 #4839

Description

@os-zhuang

#4778 的实现中发现(PR #4838),未认领,不在那条 issue 的范围内。

现象

packages/plugins/plugin-approvals/src/lifecycle-hooks.ts 有两处 admin 豁免,都读 ctx.session.roles:

  • bindApprovalLockHook 的记录锁:if (Array.isArray(roles) && roles.includes('admin')) return;
  • bindDelegationWriteGuard:if (Array.isArray(roles) && roles.includes('admin')) return;(admin 可以替他人声明 out-of-office 委托)

packages/objectql/src/engine.tsbuildSession() 逐字段构造 HookContext 的 session(userId / organizationId / positions / accessToken / isSystem / actor / skipTriggers / skipAutomations / preserveAudit),没有 roles。全仓 grep 下来:

  • session.roles 的读取方只有上面两处(都在 plugin-approvals);
  • 没有任何地方往 HookContext 的 session 上写 roles(engine.ts 里唯一的 roles:ScopedContext.execute() 传给 action 的 roles: this.context.positions,与 hook session 无关)。

rolespackages/spec/src/data/hook.zod.ts 的 HookContext session 里是声明过的(roles: z.array(z.string()).optional()),所以这是一个典型的 declared ≠ enforced:字段存在、消费方在读、生产方从不写。

后果

两处都是失败关闭,不是越权 —— 所以不是安全洞:

还有词汇不一致的问题

同一个包里,ApprovalService.isOverrideActor() 判定特权 admin 的方式是 ADR-0095 的词汇:context.permissions(admin_full_access / ORGANIZATION_ADMIN_GRANTS)、context.positions(BUILTIN_IDENTITY_PLATFORM_ADMIN / ORG_OWNER / ORG_ADMIN)、以及派生的 posture。而这两个 hook 用的是 roles.includes('admin') 这个字符串。即便把 roles 接上,它也是第二套 admin 判定方言,与 ADR-0090 D3 / ADR-0095 D3 的方向相反。

需要裁定的地方(所以这条不适合直接开修)

三种方向,选哪一种改的是公共契约:

  1. 让两个 hook 改用 ADR-0095 的判定(读 permissions / positions / posture,或直接复用 isOverrideActor 的那套判据)—— 与包内既有姿势一致,但会让 admin 覆盖从「事实上不存在」变成「真的生效」,这是一次实质的权限放宽,需要确认这正是设计意图(尤其记录锁:允许 admin 直接改被锁记录 vs 只允许通过 recall/reject 释放锁)。
  2. buildSession() 填充 roles —— 但 roles 到底映射到什么?better-auth 的成员层级(owner/admin/member)是 ADR-0057 D4 / ADR-0090 D3 / ADR-0095 D3 明令禁止作为 RBAC 权威的,填进去等于新开一条被 ADR 封掉的路。
  3. 删掉这两处 admin 豁免(ADR-0049 enforce-or-remove),记录锁的 admin 救援已经由 isOverrideActor + recall/reject 覆盖;delegation 则确认「只能本人管理自己的委托」是不是可接受的最终语义。

倾向 1 或 3,但两者对外可见行为不同,建议由维护者裁定后再开工。若最终选 3,packages/spec 里 HookContext 的 session.roles 是否一并退役(ADR-0049 / ADR-0087)也要一起决定 —— 它退役后就没有任何消费方了。

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions