# CASL 授权体系迁移文档

## 概述

NestJS 后端授权已从基于 `permissions.includes()` 的字符串匹配迁移到 CASL（`@casl/ability`）基于能力的 ABAC 授权模型。

## 架构

```
┌─────────────────────────────────────────────────────────┐
│                    AuthorizationModule (@Global)         │
│                                                         │
│  ┌──────────────────────┐  ┌────────────────────────┐   │
│  │   CaslAbilityFactory  │  │  AuthorizationService   │   │
│  │                      │  │                         │   │
│  │  createForUser(user) │  │  can(req, action, subj) │   │
│  │    → AppAbility      │  │  assert(req, ...)       │   │
│  │                      │  │  canAbility(ab, ...)    │   │
│  └──────────┬───────────┘  │  assertAbility(ab, ...) │   │
│             │               └────────────────────────┘   │
│  ┌──────────▼───────────┐  ┌────────────────────────┐   │
│  │ casl.constants.ts    │  │  PoliciesGuard          │   │
│  │  mapPermissionCode() │  │  @CheckPolicies(…)       │   │
│  │  CaslAction/Subject   │  └────────────────────────┘   │
│  └──────────────────────┘                                │
└─────────────────────────────────────────────────────────┘
```

### 核心类型

|概念|类型|说明|
|---|---|---|
|Action|`CaslAction`|`'manage' \| 'create' \| 'read' \| 'update' \| 'delete'`|
|Subject|`SubjectName`|`'Student' \| 'Room' \| 'Class' \| …` (所有实体)|
|Ability|`AppAbility`|`MongoAbility<[CaslAction, AppSubject]>`|
|User|`AuthenticatedUser`|`{ id, username, permissions, isSuperAdmin, roles }`|

### 权限码映射

|旧权限码|CASL Action|CASL Subject|
|---|---|---|
|`student:view`|`read`|`Student`|
|`student:create` / `student:import`|`create`|`Student`|
|`student:edit`|`update`|`Student`|
|`student:delete`|`delete`|`Student`|
|`student:export`|`read`|`Student`|
|`occupancy:checkin`|`create`|`Occupancy`|
|`occupancy:checkout`|`update`|`Occupancy`|
|`occupancy:transfer`|`update`|`Occupancy`|
|`bill:generate` / `bill:confirm`|`update`|`Bill`|
|`bill:export-excel` / `bill:export-pdf`|`read`|`Bill`|
|`deposit:approve`|`update`|`Deposit`|
|`sync:trigger` / `integration:trigger`|`update`|`Sync` / `Integration`|

完整映射见 `apps/server/src/authorization/casl.constants.ts`。

### 超管处理

`isSuperAdmin === true` → `ability.can('manage', 'all')` → 所有操作全部放行。

### 未知权限码处理

未知/无法映射的权限码（如 `ghost:action`）→ **不产生任何 CASL ability → deny-by-default**。用户对象上仍保留完整的 `permissions` 数组用于前端菜单/日志，但授权判断拒绝未知码。

## 修改文件清单

### 新增文件

|文件|说明|
|---|---|
|`apps/server/src/authorization/casl.constants.ts`|Action/Subject 定义、权限码映射函数|
|`apps/server/src/authorization/interfaces.ts`|`AppAbility`, `AuthenticatedUser`, `PolicyHandler` 类型|
|`apps/server/src/authorization/casl-ability.factory.ts`|CASL Ability 构建工厂|
|`apps/server/src/authorization/authorization.service.ts`|通用授权服务（HTTP + 非 HTTP）|
|`apps/server/src/authorization/authorization.module.ts`|@Global 模块|
|`apps/server/src/authorization/index.ts`|桶导出|
|`apps/server/src/authorization/decorators/check-policies.decorator.ts`|`@CheckPolicies()` 装饰器|
|`apps/server/src/authorization/guards/policies.guard.ts`|`PoliciesGuard` CASL 策略守卫|
|`apps/server/src/authorization/casl-ability.factory.spec.ts`|工厂测试（17 用例）|
|`apps/server/src/authorization/authorization.service.spec.ts`|服务测试（12 用例）|
|`apps/server/src/authorization/guards/policies.guard.spec.ts`|策略守卫测试（7 用例）|

### 修改文件

|文件|变更|
|---|---|
|`apps/server/src/auth/guards/permission.guard.ts`|注入 `CaslAbilityFactory`，用 `ability.can()` 替代 `permissions.includes()`|
|`apps/server/src/auth/guards/permission.guard.spec.ts`|新增 CASL 授权测试（7 用例）|
|`apps/server/src/app.module.ts`|导入 `AuthorizationModule`|
|`apps/server/package.json`|新增 `@casl/ability` 依赖|
|`apps/server/src/students/students.controller.ts`|注入 `AuthorizationService`，用 CASL 替代 `isSuperAdmin` 检查|
|`apps/server/src/classes/classes.controller.ts`|同上|
|`apps/server/src/attendance/attendance.controller.ts`|同上，修复测试兼容|
|`apps/server/src/schedules/schedules.controller.ts`|同上|
|`apps/server/src/dashboard/dashboard.controller.ts`|同上|

## Agent Tool 使用指南

CASL 授权服务**不依赖 HTTP ExecutionContext**，可在 Agent Tool、后台任务、CLI 等场景直接使用：

```typescript
import { CaslAbilityFactory } from './authorization';
import { AuthorizationService } from './authorization';
import { CaslAction, SubjectName } from './authorization';

// 方式 1: 只构建 Ability
const factory = app.get(CaslAbilityFactory);
const ability = factory.createForUser({
  permissions: ['attendance:view', 'attendance:create'],
  isSuperAdmin: false,
});

if (ability.can(CaslAction.Read, SubjectName.Attendance)) {
  // 执行考勤查询
}

// 方式 2: 使用 AuthorizationService
const authz = app.get(AuthorizationService);
const toolAbility = factory.createForUser(user);
authz.assertAbility(toolAbility, CaslAction.Create, SubjectName.Attendance);
// 如果无权限，抛出 ForbiddenException

// 方式 3: 通过 request-like 对象（适用于有 request 模拟的场景）
authz.assert(
  { user: { permissions: ['student:view'], isSuperAdmin: false } },
  CaslAction.Read,
  SubjectName.Student,
);
```

推荐 Agent Tool 使用 **方式 1+2**：先用 `factory.createForUser(user)` 构建 ability，再用 `service.canAbility/assertAbility` 检查。这种方式完全独立于 NestJS 请求生命周期。

## 测试命令与结果

```bash
cd apps/server

# 全部测试
npx jest --no-coverage
# 结果: 27 passed, 127 passed, 3 skipped

# 仅 CASL 相关测试
npx jest --no-coverage authorization/ auth/guards/permission.guard.spec.ts
# 结果: 54 passed

# 类型检查
npx tsc -p tsconfig.build.json --noEmit
# 结果: clean (无错误)
```

## 遗留风险 / TODO

1. **`class:edit` 宽泛授权**（ponytail 标记）：拥有 `class:edit` 权限的教师目前获得全量学生/排课/考勤管理权限。理想情况下应通过 CASL conditions 限制为仅自己班级的学生。当前数据模型（需查询 `class_teacher` 关联表确定 scope）无法直接在 CASL Ability 中表达。**未降低现有权限**，保留现状并加 TODO。

2. **前端权限守卫**：前端 `PermissionRoute` 组件（`apps/admin/src/auth/permission-store.ts`）仍然使用 `permissions.includes()` 检查。不影响安全性（后端是真实授权源），但可在后续迭代中统一。

3. **操作日志中的权限上下文**：当前操作日志记录仍使用 `user.permissions` 数组。CASL 迁移未改变日志格式。

4. **`dashboard:manage` 权限**：`dashboard` subject 在 preset permissions 中仅有 `dashboard:view`，但 dashboard.controller 检查了 `dashboard:manage`。CASL 映射将 `dashboard:manage` 的未知 action 映射为 `read`（保守），非 super_admin 用户理论上无法通过此检查。但实际上 controller 的权限守卫用的是 `@RequirePermission('dashboard:view')`，CASL 映射正常。`dashboard:manage` 仅出现在内部方法 `canManageAllDashboard` 的 permissions.includes 检查中，现已被 CASL 替代。

5. **构建验证**：`npx nest build` 未在迁移中执行（jest + tsc 已覆盖编译和类型检查）。Docker 部署前建议执行一次完整构建。

## Agent Tool 只读数据安全执行框架

### 架构

```
┌──────────────────────────────────────────────────────────────┐
│  AgentToolsModule (NON-HTTP — no controller)                  │
│                                                               │
│  ┌──────────────────────┐  ┌─────────────────────────────┐   │
│  │  AgentToolRegistry    │  │  AgentToolExecutor           │   │
│  │                      │  │                              │   │
│  │  listAvailable(ctx)  │  │  execute(name, input, ctx)   │   │
│  │    → ToolDef[]       │  │    1. assertPermission       │   │
│  │                      │  │    2. tool.validate(input)   │   │
│  │  Filtered by exact-  │  │    3. tool.execute(…)       │   │
│  │  code permission     │  │    4. audit (best-effort)    │   │
│  └──────────────────────┘  └─────────────────────────────┘   │
│                                                               │
│  Built-in tools:                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  search_students  (student:view)                      │    │
│  │  get_student_basic (student:view)                     │    │
│  └──────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────┘
```

### 安全保证

1. **动态暴露（listAvailable）**：只暴露 principal 拥有 exact permission 的 Tool。
2. **执行时二次授权（execute）**：不依赖 `listAvailable`，`execute` 再次调用 `assertPermission`。
3. **数据库 WHERE 数据范围**：`StudentAccessScope` 使用 TypeORM QueryBuilder + EXISTS 子查询，在 SQL 层面限制数据范围：
   - `manageAll`：超管或全量学生范围 → 无限制
   - `teacher`：仅 `ClassTeacher.userId` 分配班级的 active `ClassStudent`
4. **输出白名单**：所有 Tool 输出仅限 `id, name, studentNo, gender, status, organizationId, organizationName, classIds`。`phone`, `idNumber`, `emergencyContact`, `emergencyPhone` 不进入查询 SELECT。
5. **审计**：模块 `AI Agent Tool`，记录 tool 名、状态（success/denied/failed）、userId/username（来自 context principal）。审计写入失败不影响 Tool 调用结果。
6. **审计脱敏**：审计 detail 绝不包含 raw input、phone、idNumber 等敏感值。

### SDK 适配伪代码（provider-neutral）

任何 LLM SDK（Vercel AI、LangChain、OpenAI function calling 等）都可以适配：

```typescript
// 1. 获取 NestJS 容器中的 Registry 和 Executor
const registry = app.get(AgentToolRegistry);
const executor = app.get(AgentToolExecutor);
const authz = app.get(AuthorizationService);

// 2. 构建可信 AgentToolContext（userId/permissions 来自服务端认证）
const ability = abilityFactory.createForUser(authenticatedUser);
const ctx: AgentToolContext = {
  userId: authenticatedUser.id,
  username: authenticatedUser.username,
  permissions: authenticatedUser.permissions,
  isSuperAdmin: authenticatedUser.isSuperAdmin,
  ability,
};

// 3. 动态暴露工具列表（给 LLM SDK 的 tools/functions 定义）
const availableTools = registry.listAvailable(ctx);
const sdkTools = availableTools.map(tool => ({
  name: tool.name,
  description: tool.description,
  // … 根据 tool 自定义参数 schema
}));

// 4. 执行 Tool 调用（带输入校验 + 二次授权 + 审计）
const result = await executor.execute("search_students", rawInput, ctx);
// result.status: 'success' | 'denied' | 'failed'
// result.result: 白名单后的数据（仅 success 时）
// result.error:  错误信息（denied/failed 时）
```

### 新增文件清单

|文件|说明|
|---|---|
|`src/agent-tools/agent-tool.types.ts`|AgentToolContext, ToolDef, ToolExecutionResult 类型定义|
|`src/agent-tools/agent-tool.registry.ts`|Tool 注册 + 按权限过滤暴露|
|`src/agent-tools/agent-tool.executor.ts`|执行时二次授权 + 输入校验 + 审计|
|`src/agent-tools/tools/search-students.tool.ts`|search_students Tool|
|`src/agent-tools/tools/get-student-basic.tool.ts`|get_student_basic Tool|
|`src/agent-tools/agent-tools.module.ts`|NestJS 模块（不暴露 HTTP endpoint）|
|`src/agent-tools/index.ts`|桶导出|
|`src/students/student-access-scope.ts`|StudentAccessScope 数据范围类型|
|`src/agent-tools/agent-tool.executor.spec.ts`|执行器测试（19 用例）|
|`src/agent-tools/tools/search-students.tool.spec.ts`|search_students 测试（12 用例）|
|`src/agent-tools/tools/get-student-basic.tool.spec.ts`|get_student_basic 测试（10 用例）|
|`src/students/students.agent-api.spec.ts`|agent-safe API 测试（17 用例）|

### 修改文件

|文件|变更|
|---|---|
|`src/authorization/authorization.service.ts`|新增 `canPermission` / `assertPermission` 方法|
|`src/authorization/authorization.service.spec.ts`|新增 8 个 exact-code 权限检查测试|
|`src/students/students.service.ts`|新增 `agentSearchStudents` / `agentGetStudentBasic` + `applyStudentScope`|
|`src/app.module.ts`|导入 `AgentToolsModule`|
|`docs/superpowers/plans/casl-migration.md`|本文档新增 Agent Tool 章节|

### 测试结果

```bash
npx jest --no-coverage --forceExit
# 结果: 31 suites, 223 passed, 3 skipped

npx tsc -p tsconfig.build.json --noEmit
# 结果: clean

npx nest build
# 结果: clean

npx eslint --no-fix src/agent-tools/**/*.ts src/students/student-access-scope.ts src/authorization/authorization.service.ts
# 结果: clean
```

### 剩余风险

1. **classIds 聚合为第二查询**：对大量结果，批量聚合 classIds 的第二条查询使用 `IN (:...ids)`，在 MySQL 中 IN 子句过大时有性能上限（当前 limit 50 安全）。
2. **`manageAll` 判定**：当前 `manageAll` = `isSuperAdmin`。若未来有非超管的全量学生范围角色，需扩展 `StudentAccessScope` 的 `manageAll` 判定逻辑。
3. **Tool 扩展**：当前仅 `student:view` 的两个 Tool。新增 Tool 只需实现 `ToolDef` 并注册到 `AgentToolsModule`，无需修改框架代码。
