---
date: 2026-05-04
tags: [iam, auth, cache, security, role-mutation, p0-bug]
related: backend/src/modules/organization/users/users.service.ts, backend/src/modules/organization/auth/services/auth-cache.service.ts
---

# IAM 角色 mutation 必须 invalidate auth cache

## 背景

`AuthCacheService` 在 Redis 里缓存 user 权限 5 分钟（撤权 SLA）。设计假定：**任何角色/权限变更都主动调 `authCache.invalidate(userId)`**——cache TTL 仅是兜底。但 `UsersService` 5 个 role-mutation 方法**全部漏调** invalidate（4 处留着 `// TODO: Clear permission cache` 注释，1 处连 TODO 都没写）。

## 现象（FFAI UAT 实测）

- 09:25:18 chentao.jia 登录 → cache build = `systemRoles.roles=['RobotManagerRLE']`
- 09:26:01 admin 把 `MeetingManager` 加给 chentao.jia（前端 `addRoles` 调用）
- 09:26:50 chentao.jia 创建会议 → **403 Forbidden**
- 09:26-09:28 7 次重试都 403
- 09:28:24 itadmin 同样请求 → 201

DB 里 chentao.jia 确实有 MeetingManager 角色（user_role_rel.created_at = 09:26:01）。但 JwtStrategy 命中陈旧 cache，systemRoles.roles 仍是登录时的快照。

## 5 个漏调点

`backend/src/modules/organization/users/users.service.ts`：

| 方法 | 业务 | 安全等级 |
|---|---|---|
| `terminateUser` | 离职 | **P0** — 离职用户 5 分钟内仍能操作 |
| `assignRoles` (PUT 全替换) | 设置用户角色为这些 | **P0** — 撤掉的角色 5 分钟仍生效 |
| `addRoles` (POST 增量加) | 把这些追加到用户现有角色 | P1 — 新权限滞后 5 分钟生效（chentao.jia 这次踩的）|
| `removeRole` (兼容旧接口，删该角色所有 region) | 撤角色 | **P0** — 撤掉的角色 5 分钟仍生效 |
| `removeRegionRole` | 撤区域角色 | **P0** — 同上 |
| `assignRegionRoles` (全替换) | 区域角色全替换 | **P0** — 同上 |
| `addRegionRole` | 加区域角色 | P1 — 同 addRoles |

撤权场景全部是真实安全风险；新增场景是用户体验问题，但同样违反 SLA 承诺。

## 修复

每个 mutation 方法在 DB 写入完成后加：

```ts
await this.authCache.invalidate(userId);
```

注入路径**零摩擦**：`OrganizationModule` 已经 import `AuthModule`，`AuthModule` 已经 export `AuthCacheService`，UsersService 直接构造函数注入即可，**不需要 forwardRef、不需要新建模块**。

## 检测方法（下次类似情况复用）

权限相关 bug 看不到 stack 时，**用 DB 时间戳对比 cache 时间戳**：

```sql
-- 用户角色变更时间
SELECT r.code, ur.created_at FROM platform_iam.user_role_rel ur
JOIN platform_iam.roles r ON r.id = ur.role_id
WHERE ur.user_id = '<uid>' ORDER BY ur.created_at;
```

跟 backend log 里 `Login successful for user` 的时间对比：
- DB role created_at **>** login 时间 → cache 是陈旧的 → 这就是 root cause
- DB role created_at **<** login 时间 → cache 应该是 fresh，要查别处

`Redis.EXISTS user:${uid}:auth = 0` 不代表 cache 当时也不存在——查询时刻 TTL 可能已到期。要看**报错时刻**的 cache 状态，靠日志时间戳推断。

## 不直观的点

1. **`// TODO: Clear permission cache` 注释 = 4 个真实的 P0 bug**——TODO 不是占位符，是显式的"安全 SLA 没履行"。Code review 看到这种 TODO 必须严肃对待。
2. **`AuthCacheService.ttlSec = 5 * 60` (5 分钟) 不是"刷新频率"，是"缺少主动 invalidate 时的退化兜底"**。设计意图是 mutation 时立即失效；TTL 只是边界保险。
3. **NestJS DI 怎么避免循环**：先看依赖图。如果 A 不直接 import B（A 依赖的 module import 了 B），通过 module exports 注入即可，无需 forwardRef。本次 UsersService → AuthCacheService 走的就是 OrganizationModule import AuthModule，AuthModule export AuthCacheService 这条路径。
4. **重新登录是"绕过 cache 陈旧"的有效用户侧 workaround**——但不能依赖用户主动重登。fix 必须在后端 mutation 时主动 invalidate。

## Self-improvement candidate

可以提取为 skill 或加到 backend-main 的 references：**「写 IAM mutation 时检查 cache invalidate」 = 一条标准 review checkpoint**。任何改 user_role_rel / role / permission 关系表的 service 方法，必须有对应 cache invalidate 调用。
