---
date: 2026-05-18
type: error
tags: [iam, rbac, admin-role, integration-test-vs-prod, agent]
---

# Admin endpoint 403：itadmin 的全局 Administrator 不在 `actor.organizationRoles[orgId]`

## 现象

集成测试 `[AGENT-MEM-013]` 用 itadmin token POST `/agent/admin/memories` → 201 通过。
然而**在真实 slot dev 环境**用同样的 itadmin / 同样的 token 调同一个 endpoint → **403
"admin role required for org-shared memory"**。

```
POST /api/v1/agent/admin/memories → 403
  message: admin role required for org-shared memory
```

## 根因

`assertAdminRoleInOrg(req, orgId)` 只查了 `actor.organizationRoles[orgId]`：

```ts
const rolesInOrg = actor?.organizationRoles?.[orgId] ?? [];
const ok = rolesInOrg.some((r) => allowed.includes(r));
```

但 itadmin 的 `Administrator` 角色是 `init-itadmin.ts` 里以 **`organizationId: null` 全局
角色** 分配的（注释明确写"全局管理员角色，在所有组织生效"）。auth.service 的
`sliceAuthPayload` 把它放在 **顶层 `roles`** 数组（系统角色 + 当前 org 角色合集），
**不放进 `organizationRoles[orgId]` map**——后者只含**显式授予到该 org** 的角色。

集成测试为什么过？因为 `setupIntegrationTest` 创建的 admin 用户是**当 org 的 admin
（org 显式授角色）**，所以 `organizationRoles[orgId]` 里直接含 `Administrator`。生产
itadmin 是全局角色，路径不一样。**集成测试用例覆盖了部署用例的 90%，但 itadmin
这条 10% 路径没覆盖**。

## 解法

`assertAdminRoleInOrg` 三层来源择一通过：

```ts
function assertAdminRoleInOrg(req, orgId) {
  const actor = getActor(req);
  const topRoles = (actor as { roles?: string[] }).roles ?? [];       // ← 系统级（关键！）
  const rolesInOrg = actor?.organizationRoles?.[orgId] ?? [];
  const permissions = actor?.permissions ?? [];
  const allowed = ['Administrator', 'ITAdmin', 'agent.admin'];
  const ok =
    topRoles.some(r => allowed.includes(r)) ||
    rolesInOrg.some(r => allowed.includes(r)) ||
    permissions.includes('agent.admin') ||
    permissions.includes('admin');
  if (!ok) throw new ForbiddenException(...);
}
```

## 工程化保险

- **agent 模块的 `AgentActor` interface 应该补 `roles?: string[]`** —— 当前类型不完整
  让 TS 不能在编译期发现"漏看顶层 roles"问题；要 cast 才能用。
- **集成测试 setup 应该专门覆盖"全局角色 user"**：加一个 helper 像
  `createGlobalAdminUser()` 模拟 itadmin 风格的全局 Administrator，跟 per-org admin 测试用例并行。
- **更长远**：搞一个 `@RequireAdmin()` 装饰器或 RoleGuard，统一三层来源判定逻辑，
  本 controller-local 函数退役。

## 教训

1. **集成测试 setup 决定了"被测的部署形态"**。setupIntegrationTest 给 admin user
   per-org 角色是合理的，但**不等价于生产 itadmin 全局角色**——这条诊断路径
   不会出现在测试里
2. **`organizationRoles` 和 `roles` 是两层不同语义**：前者是 per-org 显式授权，
   后者是"系统级 + 当前 org" 合集。任何 admin 校验都要看两个，不只一个
3. **集成测试通过 ≠ 真实环境通过**：集成测试只能锁住"在 setup 形态下"的逻辑，
   不能锁住"所有合法用户形态都能用"
4. **AgentActor 类型不完整是结构性 bug**：项目里 `req.user` 真实形状由
   `auth.service.sliceAuthPayload` 定义，跟 `auth-resolution.util.ts` 的 interface
   分了两份。**类型应该从 sliceAuthPayload return type 反向导出**，避免漂移

## 关联

- [ERR-20260518-004](ERR-20260518-004-reset-db-missing-organization-seed.md)：reset DB 后所有 API 400；同一根因家族（IAM/org 解析路径）
- CLAUDE.md「核心不变量 INV-1」：跨 org 数据隔离 —— 但 admin 操作要正确识别"哪些 user 算 admin"
- 修复 commit：本 PR
