# IAM 实施 TODO（代码层）

> 规则层全量定义在 `09-iam-security.md`，不分 V1/V2/V3/V4。
> 本文件只追踪实施进度，按优先级排序。**所有项完成后才算符合规则。**

---

## ✅ Status：PR #138 已落地主体（2026-04-25）

**[PR #138](http://43.130.59.228/FFAIWorkspace/workspace/pulls/138)** 已合并到 develop，落地了本文件 P0 + P1 + 大部分 P2 项。详细测试覆盖见 `testing/reports/pr138-pre-merge-checklist.md`（39/40 通过）。

**已完成的标志性能力**：
- DataScopeInterceptor 全局注册 + 9 个 IAM service 落地（DataScope / OrganizationContext / PermissionDelegation / AccessReview / IamAudit / FieldPermission / Masking / SystemPrincipal / EmergencyBypass）
- 老装饰器（`@RequireOrganizationPermissions` / `@RequireGlobalPermissions` / `@Roles`）export 已删除，所有 controller 改用 `@RequirePermissions`
- JWT payload 瘦身到 `{ sub, jti, exp }`、Refresh token Redis opaque + rotation、ThrottlerGuard 限流、JTI 黑名单 Redis 存储 + fail-secure
- AuthCacheService（5min TTL）+ inboundDelegationScopes 一并缓存
- assertAccess 静态契约脚本接 pre-commit
- iam_audit_log 表 + IamAuditService.record（hash chain 链式签名预留）
- 前端三级保护（PermissionGuard / Can / Navigation）+ ForbiddenPage / NotFoundPage / SystemRecordBadge
- 一次性总迁移 `20260425085204_iam_full_terminal_state` + 17 决策 × 22 禁止事项自查

**未完成 / 留给后续 PR**：
- MFA 集成（`MfaService` 已注册到 `AuthModule` 但**没挂入 login 流程**）
- DataScopeInterceptor / FieldPermission / EmergencyBypass / SystemPrincipal 的**业务模块接入**（PR 只搭框架，0 业务调用点）
- AccessReview cron 告警通道（当前仅 logger.warn，TODO 接 Notification）
- 历史命名（parts/org/site-attendance/robot-manager × 182 处）批量改 snake_case，见 `docs/todos/iam-data-scope-naming-debt.md`
- 完整 i18n 化的两个 IAM 治理页（PR 已加基础 i18n，未来精细化）

下文具体打勾留给 retro 时逐项核对。

---

## P0 — 立即修复（阻塞上线）

- [ ] **注册 DataScopeInterceptor 到全局**
  `app.module.ts` 当前未注册 `DataScopeInterceptor`，读路径数据过滤完全没生效。
  补 `{ provide: APP_INTERCEPTOR, useClass: DataScopeInterceptor }`。

- [ ] **补 `departments` 控制器缺失的权限装饰器**
  `departments.controller.ts` 的 6 个 GET 端点无 `@RequirePermissions`（`@Get()`、`@Get('tree')`、`@Get('organizations')`、`@Get(':id')`、`@Get(':id/members')`、`@Get(':id/stats')`）。
  PermissionsGuard 对无装饰器端点直接放行（文档 4.7 的明确禁止），导致任意登录用户可枚举组织部门结构。

- [ ] **写操作铺设 `assertAccess`（高敏感模块先行）**
  全系统 `grep assertAccess` 零调用。先补：`user` / `role` / `permission` / `department` / `position` / `approval`。
  每个模块配套集成测试："A 用户用 B 的 id 改/删 → 403"。

- [ ] **`CUSTOM` 类型改抛异常**
  `data-scope.service.ts:129-130` 当前 `case DataScopeType.CUSTOM: return {}`（静默等价于 ALL，极危险）。
  改为 `throw new NotImplementedException('CUSTOM DataScope 在 V1 不支持')`。
  `data-scopes.seed.ts` 里加 assertion：禁建 CUSTOM 记录。

- [ ] **Token 黑名单接 Redis**
  `auth.service.ts:10` 是内存 `Set<string>`，集群不一致、重启丢失。
  JTI 做 key，TTL = token 剩余有效期。

## P1 — 两周内

- [ ] **多角色合并候选池验证**
  验证 `DataScopeService.matchScope()` 确实把精确匹配 + `*` 兜底共同取最宽，补集成测试（HrManager + DepartmentManager 查 parts 应得 DEPARTMENT_TREE）。

- [ ] **清理老装饰器遗留**
  `users.controller.ts:111, 125` 仍在用 `@RequireOrganizationPermissions`。
  `permissions.decorator.ts:59, 77` 源头的 `RequireOrganizationPermissions` / `RequireGlobalPermissions` export 改为 throw deprecated，或直接删除。

- [ ] **Administrator 角色名统一**
  `permissions.guard.ts` 用 `Administrator`，`users.service.ts` 接受 `['ADMIN', 'Administrator']` 两种。
  统一为 `Administrator`，所有检查点 `.toUpperCase() === 'ADMINISTRATOR'`。

- [ ] **`@Public()` 使用审计**
  全局 grep `@Public()`，列出所有端点，人工审一遍是否有误挂到敏感接口。

- [ ] **validateUser 深 join 优化**
  当前每个请求都加载 `roles → permissions + dataScopes` 三层 include。
  把 user auth payload 缓存到 Redis（`user:${userId}:auth`，TTL 5min），role/scope 变更时失效。

- [ ] **DEPARTMENT_TREE 递归缓存**
  `organization-context.service.ts` 无缓存、无防 N+1。
  Redis 缓存 `dept_tree:${deptId}`（TTL 1h），部门 CRUD 时失效。如 Prisma 不便，走原生 `WITH RECURSIVE`。

- [ ] **缓存一致性：权限更新生效延迟**
  当前 dataScopes 在 token 内缓存，7d 期内都是老值。
  跟 validateUser 优化一起做：JWT 只存 userId，权限数据每请求查 Redis（+1 次 Redis，可接受）。

## P2 — 排期做

- [ ] Administrator 操作审计日志（guard bypass 必记）
- [ ] ForbiddenException 带结构化字段（userId / endpoint / 缺哪条权限）
- [ ] `resource` 改用 enum，避免 typo 静默降级为 SELF
- [ ] `RoleDataScope` 加 `@@index([dataScopeId])` 等缺失索引
- [ ] 紧急豁免机制（Redis 白名单 + 4h 自动关闭）
- [ ] 灰度 / 回滚 runbook

## 规则决议带来的新增实施项

由 `09-iam-security.md` 一次定义终态规则派生，按 Layer 分组：

### Layer 1（Token & 身份认证）
- [ ] JWT payload 瘦身：只存 `userId / exp / jti`，去除 permissions / dataScopes 字段
- [ ] Access Token 改 30min
- [ ] Refresh Token 30d + Redis 存储 + 主动撤销端点
- [ ] 登录限流（@nestjs/throttler，30s ≤ 5 次）
- [ ] MFA（TOTP，敏感操作触发）
- [ ] 异地登录二次验证

### Layer 3 / Layer 4 加载机制
- [ ] 权限与 DataScope 从 DB 加载后按组织分层结构化，写入 Redis `user:${userId}:auth`（TTL 5min）
- [ ] 请求时按 `currentOrg` 切片 systemRoles + orgRoles[currentOrg]
- [ ] 配置变更事件 `DEL user:${affectedUserIds}:auth`
- [ ] 解雇事件：清 `user:${userId}:auth` + invalidate `active_jtis` 所有 token
- [ ] `user:${userId}:active_jtis` 列表机制

### Layer 4 配置治理
- [ ] 配置 ceiling 校验：角色管理模块的分配 DataScope 接口，Service 层校验"≤ 自己 scope"
- [ ] DataScope 配置变更审计：对 `data_scopes` / `role_data_scope_rel` / `role_permission_rel` / `user_role_rel` 增删改记 AuditLog
- [ ] 主动失效触发：配置变更 webhook → 清 Redis 缓存

### Layer 4 语义落地
- [ ] `DataScopeService.resolve`：按 resource 独立合并，精确匹配 + `*` 兜底共同候选池
- [ ] 跨组织不合并：加载时按 `UserRole.organizationId` 分层存储
- [ ] `DEPARTMENT` scope：取 `leftAt IS NULL` 所有部门的并集
- [ ] `ORGANIZATION` scope WHERE：`organizationId = currentOrg OR organizationId IS NULL`
- [ ] 读路径单条查询必带 `dataScopeFilter` → 查不到返回 404
- [ ] 写路径单条查询**不带** `dataScopeFilter` → 按 404/403 区分返回
- [ ] 写路径 `organizationId` 由系统填充，客户端传 null 视为错误

### 字段级权限 & 脱敏（基础设施）
- [ ] `FieldPermission` 模型 + 迁移
- [ ] `FieldPermissionService.filter(user, resource, record)` 实现
- [ ] 脱敏规则（手机号/邮箱/身份证/银行账号）
- [ ] `@Sensitive` / `@Masked` 装饰器

### 前端三级保护
- [ ] 所有页面包裹 `<PermissionGuard>`
- [ ] 所有操作按钮包裹 `<Can>`
- [ ] `navigation.ts` 全部菜单项配 permissions
- [ ] 404 vs 403 UI 差异化（隐私优先：不告诉用户"存在"）

### Resource 命名合规
- [ ] 现有 `@DataScope` / `RoleDataScope.resource` 统一到 snake_case 名词单数
- [ ] seed 验证脚本：拒绝未被权限码覆盖的 resource 名
- [ ] 文档 `data-scope-fields-check.ts` 校验扩展（可选）

### 非用户主体与委托（新规则 §5.3.13 / §5.3.14）
- [ ] 异步任务基础设施：task payload 加 `{ userId }` 或 `{ principal: 'system', source }` 字段；消费者启动前校验；缺失抛 `MissingActorException`
- [ ] System Principal 操作审计日志
- [ ] `UserDelegation` 模型 + 迁移 + 索引
- [ ] 委托 scope 合并逻辑（代理人请求时加载委托人 scope，作为附加来源并入）
- [ ] 委托 ceiling 校验（委托 scope ≤ 委托人自己）
- [ ] 委托撤销触发 Redis 缓存失效
- [ ] 委托审计：代理操作审计日志标记 `on_behalf_of` + `delegation_id`
- [ ] 禁止链式委托的运行时校验

### 定期访问复核（新规则 §5.3.15）
- [ ] `UserRole.lastReviewedAt` / `lastReviewedBy` / `reviewComment` 字段 + 迁移
- [ ] 模块 owner UI：列出待复核项，支持逐项"保留 / 撤销"
- [ ] 定时任务扫描超过 90 天未复核项，告警
- [ ] 复核动作审计日志
- [ ] 高敏感模块（审批 / 财务 / HR / 用户 / 权限）纳入强制复核流程

### 部门树超限（新规则 §5.3.16）
- [ ] `OrganizationContextService` 超过 `maxDepth` 抛 `DepartmentTreeTooDeepException`
- [ ] 异常上报告警通道
- [ ] 单元测试覆盖：10 层正常 / 11 层超限 / 循环引用检测

### 可观测（新规则 §5.3.17）
- [ ] `DataScopeInterceptor` 匹配不到 resource 时 WARN 日志 + metric
- [ ] Metric `data_scope.unknown_resource` 接入 Grafana 告警
- [ ] 日志字段：`userId / endpoint / declaredResource / availableResources`

### Redis 高可用
- [ ] Redis Sentinel / Cluster 部署
- [ ] 客户端重试 + 断路器配置
- [ ] 滚动升级 runbook

### 全局表读写边界（§5.3.4 升级版）
- [ ] 写路径服务端覆盖 `organizationId`（不是拒绝，是覆盖）
- [ ] 专用 `POST /system/...` 接口族，`@RequirePermissions('system:admin')` 保护
- [ ] 前端区分全局记录（带"系统内置"标签）+ 编辑按钮对全局记录禁用或隐藏
- [ ] 错误码 `SYSTEM_RECORD_READ_ONLY`

### 静态契约检查（§7.1 升级版）
- [ ] CI / pre-commit 扫描 `prisma.xxx.update(` / `delete(` → 所在函数必有 `assertAccess` 或 `@SkipAssertAccess`
- [ ] `@SkipAssertAccess('理由')` 装饰器实现 + 理由必填的校验
- [ ] 扫描脚本可扩展（基于现有 `testing/scripts/` 架构）
