Files
gongxue-base/docs/superpowers/plans/casl-migration.md

283 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 SDKVercel 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. 构建可信 AgentToolContextuserId/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`,无需修改框架代码。