feat: add CASL authorization and AI configuration
This commit is contained in:
282
docs/superpowers/plans/casl-migration.md
Normal file
282
docs/superpowers/plans/casl-migration.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 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`,无需修改框架代码。
|
||||
Reference in New Issue
Block a user