Files
Edu/docs/security/graphql-auth-audit-2026-07.md
SpecialX 315b954998 docs(docs): add GraphQL @auth audit report
Audits 50 resolvers across 8 Apollo Federation subgraphs (iam,
config-service, classes, core-edu, content, msg, data-ana, ai).
Coverage: 18 guarded, 32 missing (36%). TS subgraphs: 35 total,
16 guarded, 19 missing (45.7%). Python subgraphs: 15 total, 2
guarded, 13 missing (13.3%). Documents AuthMiddleware /graphql
coverage gaps and Python resolver permission infrastructure as
follow-up items.
2026-07-17 13:20:30 +08:00

295 lines
22 KiB
Markdown
Raw Permalink 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.
# GraphQL 字段级 @auth 审计报告2026-07
> 版本v1.0
> 日期2026-07-17
> 范围portal-shell 数据抽象层与 GraphQL 安全加固 plan Task 15
> 关联:
>
> - [Plan](../superpowers/plans/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening.md)
> - [Spec §5.2](../superpowers/specs/2026-07-17-portal-shell-data-abstraction-and-graphql-hardening-design.md)
> - [项目规则 §3.8 Controller 规范](../../.trae/rules/project_rules.md)
---
## 1. 审计范围
8 个 Apollo Federation 子图的所有 GraphQL Query / Mutation / @ResolveField / Federation resolveReference
| 子图 | 语言 | GraphQL 库 | resolver 目录 |
| -------------- | ---------- | ------------------------------------- | -------------------------------------------------- |
| iam | TypeScript | @nestjs/graphql + Apollo Federation 2 | `services/iam/src/graphql/resolvers/` |
| config-service | TypeScript | @nestjs/graphql + Apollo Federation 2 | `services/config-service/src/graphql/resolvers/` |
| classes | TypeScript | 无 GraphQL仅 REST | `services/classes/src/classes/`HTTP Controller |
| core-edu | TypeScript | @nestjs/graphql + Apollo Federation 2 | `services/core-edu/src/graphql/resolvers/` |
| content | TypeScript | @nestjs/graphql + Apollo Federation 2 | `services/content/src/graphql/resolvers/` |
| msg | TypeScript | @nestjs/graphql + Apollo Federation 2 | `services/msg/src/graphql/resolvers/` |
| data-ana | Python | strawberry-graphql + Federation 2 | `services/data-ana/src/data_ana/graphql/schema.py` |
| ai | Python | strawberry-graphql + Federation 2 | `services/ai/src/ai/graphql/schema.py` |
**classes 说明**classes 服务当前未暴露 GraphQL 端点,仅有 HTTP REST Controller
`ClassesController`),其所有路由已通过 `@RequirePermission(Permissions.CLASSES_*)`
装饰器声明权限点。本次审计在 classes 子图无 GraphQL resolver 可审。
---
## 2. 审计方法
1. 运行 `pnpm run arch:scan` 更新 arch.dbTypeScript 20 模块 / 4801 符号、Go 2 模块 / 89 符号、Python 2 模块 / 559 符号、Protobuf 497 契约)
2. 通过 Grep 在 `services/*/src/graphql/resolvers/*.resolver.ts``services/*/src/**/graphql/schema.py` 中搜索 `@Query``@Mutation``@ResolveField``@ResolveReference``@strawberry.field``resolve_reference` 等装饰器与字段
3. 对每个 resolver 读取源码,核查是否具备以下任一守卫:
- `@RequirePermission(...)` 装饰器TS 子图)
- 自定义内联权限校验(如 dataScope 的 self/admin 检查)
- Federation `@ResolveReference()` / `resolve_reference`(内部引用,不需守卫)
4. 检查各子图 `app.module.ts` 是否注册 `APP_GUARD` 全局 `PermissionGuard``RouterAuthGuard`,以及 `AuthMiddleware` 是否覆盖 `/graphql` 路径
5. 检查 Python 子图是否有 `require_permission` 装饰器或等价机制应用到 GraphQL resolver
---
## 3. 审计结果
### 3.1 iam 子图
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| ----------------- | ---------------- | ----------------- | --------------------------------------- | ---------------------------------------- | ---- |
| UserResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| UserResolver | user | @Query | 无 | @RequirePermission(IAM_USER_READ) | ❌ |
| RoleResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| RoleResolver | role | @Query | 无 | @RequirePermission(IAM_USER_READ) | ❌ |
| DataScopeResolver | dataScope | @Query | 自定义内联校验self/admin返回 null | 自定义内联校验(保留,避免破坏自助查询) | ✅ |
### 3.2 config-service 子图
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| ---------------------- | ------------------ | ----------------- | --------------------- | ------------------------------- | ---- |
| LayoutTemplateResolver | layoutTemplates | @Query | 无 | @RequirePermission(CONFIG_USER) | ❌ |
| PluginConfigResolver | pluginConfig | @Query | 无 | @RequirePermission(CONFIG_USER) | ❌ |
| UserLayoutResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| UserLayoutResolver | userLayoutOverride | @Query | 无 | @RequirePermission(CONFIG_USER) | ❌ |
| PluginResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| PluginResolver | plugin | @Query | 无 | @RequirePermission(CONFIG_USER) | ❌ |
| PluginResolver | plugins | @Query | 无 | @RequirePermission(CONFIG_USER) | ❌ |
### 3.3 classes 子图
无 GraphQL resolver仅 HTTP REST Controller已有 @RequirePermission 覆盖)。
### 3.4 core-edu 子图
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| ------------------- | ---------------- | ----------------- | --------------------- | --------------------------------- | ---- |
| ClassResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| ClassResolver | classInfo | @Query | 无 | @RequirePermission(CLASS_READ) | ❌ |
| StudentInfoResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| DataScopeResolver | visibleGrades | @ResolveField | 无 | @RequirePermission(GRADE_READ) | ❌ |
| DataScopeResolver | visibleExams | @ResolveField | 无 | @RequirePermission(EXAM_READ) | ❌ |
| ExamResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| ExamResolver | exam | @Query | 无 | @RequirePermission(EXAM_READ) | ❌ |
| GradeResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| GradeResolver | grade | @Query | 无 | @RequirePermission(GRADE_READ) | ❌ |
| HomeworkResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| HomeworkResolver | homework | @Query | 无 | @RequirePermission(HOMEWORK_READ) | ❌ |
### 3.5 content 子图
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| ---------------------- | ---------------- | ----------------- | --------------------- | ------------------------------------------------ | ---- |
| ChapterResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| ChapterResolver | chapter | @Query | 无 | @RequirePermission(CONTENT_CHAPTER_READ) | ❌ |
| KnowledgePointResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| KnowledgePointResolver | knowledgePoint | @Query | 无 | @RequirePermission(CONTENT_KNOWLEDGE_POINT_READ) | ❌ |
| QuestionResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| QuestionResolver | question | @Query | 无 | @RequirePermission(CONTENT_QUESTION_READ) | ❌ |
| TextbookResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| TextbookResolver | textbook | @Query | 无 | @RequirePermission(CONTENT_TEXTBOOK_READ) | ❌ |
### 3.6 msg 子图
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| -------------------- | ---------------- | ----------------- | --------------------- | ------------------------------------------- | ---- |
| NotificationResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| NotificationResolver | notifications | @Query | 无 | @RequirePermission(MSG_NOTIFICATION_READ) | ❌ |
| TemplateResolver | resolveReference | @ResolveReference | 无Federation 内部) | 无 | ✅ |
| TemplateResolver | template | @Query | 无 | @RequirePermission(MSG_NOTIFICATION_MANAGE) | ❌ |
### 3.7 data-ana 子图Python / Strawberry
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| -------- | ----------------- | ----------------- | -------- | ----------------------------------------------- | ---- |
| Query | class_performance | @strawberry.field | 无 | require_permission(analytics:class:read) | ❌ |
| Query | student_weakness | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
| Query | learning_trend | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
| Query | teacher_dashboard | @strawberry.field | 无 | require_permission(analytics:teacher:dashboard) | ❌ |
| Query | student_dashboard | @strawberry.field | 无 | require_permission(analytics:student:dashboard) | ❌ |
| Query | parent_dashboard | @strawberry.field | 无 | require_permission(analytics:parent:dashboard) | ❌ |
| Query | admin_dashboard | @strawberry.field | 无 | require_permission(analytics:admin:dashboard) | ❌ |
| Query | student_mastery | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
| Query | mastery_summary | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
| Query | error_book_items | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
| Query | error_book_stats | @strawberry.field | 无 | require_permission(analytics:student:read) | ❌ |
**说明**data-ana 使用 Strawberry GraphQLPython不使用 NestJS `@RequirePermission` 装饰器。
当前已有 `RouterAuthMiddleware`(校验 Router-Authorization Header+ service 层 `DataScope` 过滤,
但无字段级权限校验。补齐需要在 `schema.py` 中引入 `PermissionGuard` 机制
Strawberry middleware 或 resolver 内联校验),属于 Task 16 范围之外的基础设施工作。
### 3.8 ai 子图Python / Strawberry
| Resolver | 字段 | 装饰器类型 | 当前守卫 | 期望守卫 | 状态 |
| ---------------- | ------------------ | -------------------------------------- | --------------------- | -------------------------------------- | ---- |
| GeneratedReport | resolve_reference | @strawberry.federation.type cls method | 无Federation 内部) | 无 | ✅ |
| LessonPlanStatus | resolve_reference | @strawberry.federation.type cls method | 无Federation 内部) | 无 | ✅ |
| Query | lesson_plan_status | @strawberry.field | 无 | require_permission(ai:lesson:generate) | ❌ |
| Query | generated_report | @strawberry.field | 无 | require_permission(ai:report:generate) | ❌ |
**说明**ai 服务已定义 `require_permission` 装饰器(`middleware/permission.py`),但该装饰器
面向 FastAPI 路由(从 kwargs 取 `user_context`),不适用于 Strawberry resolver`info: Info`)。
`Query.lesson_plan_status` / `Query.generated_report` 未注入 `info: Info`
无法获取 UserContext。补齐需要在 schema.py 中增加 `info: Info` 参数并改造装饰器,
属于 Task 16 范围之外的基础设施工作。
---
## 4. 统计
| 指标 | 数量 | 备注 |
| -------------- | ---- | -------------------------------------------------------------------------------------------------------------------- |
| 总 resolver 数 | 50 | 含 Federation resolveReference10+ 自定义内联守卫1+ 面向用户字段39 |
| 已有守卫 | 18 | 10 resolveReference + 1 iam.dataScope 自定义内联 + 7Python resolve_reference ×2 与 iam resolveReference 重叠计入) |
| 缺失守卫 | 32 | TS 19 + Python 13 |
| 覆盖率 | 36% | 18/50 |
**TS 子图统计**Task 16 补齐范围):
| 指标 | 数量 |
| -------------- | ----- |
| TS 总 resolver | 35 |
| 已有守卫 | 16 |
| 缺失守卫 | 19 |
| 覆盖率 | 45.7% |
**Python 子图统计**Task 16 范围外,需独立基础设施工作):
| 指标 | 数量 |
| ------------------ | ----- |
| Python 总 resolver | 15 |
| 已有守卫 | 2 |
| 缺失守卫 | 13 |
| 覆盖率 | 13.3% |
---
## 5. 关键发现
### 5.1 全局 Guard 已注册但 opt-inTS 子图)
所有 6 个 TS 子图均在 `app.module.ts` 注册了 `APP_GUARD` with `PermissionGuard``RouterAuthGuard`
- `RouterAuthGuard`:校验 `Router-Authorization` Header生产环境仅允许 Apollo Router 调用子图)
- `PermissionGuard`**opt-in 模式**——无 `@RequirePermission()` 装饰器时直接 `return true`
不会自动拒绝未声明权限点的 resolver
因此,全局 Guard 的存在**不等于**字段级守卫全覆盖。本次审计将所有未显式声明
`@RequirePermission` 的面向用户 resolver 标记为 ❌Task 16 会逐个补齐。
### 5.2 AuthMiddleware 未覆盖 /graphql重要
`PermissionGuard` 依赖 `request.userId` / `request.userRoles`,这两个字段由 `AuthMiddleware`
`x-user-id` / `x-user-roles` Header 注入。但各子图 `app.module.ts``AuthMiddleware`
的注册范围不一致:
| 子图 | AuthMiddleware 注册范围 | /graphql 是否覆盖 | Task 16 后生产环境能否生效 |
| -------------- | ------------------------------------ | ----------------- | -------------------------- |
| iam | `v1/iam/me`, `v1/iam/logout` 等 REST | ❌ 未覆盖 | ⚠️ 需补注册 AuthMiddleware |
| config-service | `v1/config/*` REST | ❌ 未覆盖 | ⚠️ 需补注册 AuthMiddleware |
| classes | 无 GraphQL | N/A | N/A |
| core-edu | `forRoutes("*")` 排除 `/healthz` | ✅ 覆盖 | ✅ 可生效 |
| content | 未注册 AuthMiddleware | ❌ 未覆盖 | ⚠️ 需补注册 AuthMiddleware |
| msg | 未注册 AuthMiddleware | ❌ 未覆盖 | ⚠️ 需补注册 AuthMiddleware |
**影响**Task 16 为 iam / config-service / content / msg 补齐 `@RequirePermission` 装饰器后,
生产环境(`DEV_MODE=false`)下 `PermissionGuard` 会因 `request.userId` 未注入而拒绝请求
iam 抛 `PermissionDeniedError("missing user identity")`;其他子图 roles 为空导致权限不匹配)。
开发环境(`DEV_MODE=true`)下 Guard 直接放行,不受影响。
**建议**:补齐装饰器后,需在后续 task 中为 iam / config-service / content / msg 的
`app.module.ts` 追加 `consumer.apply(AuthMiddleware).forRoutes("graphql")`,使 Guard 能读取
用户身份。该修改超出 Task 16 约束(仅修改 resolver 文件 + 创建 permission.guard.ts
作为 follow-up 项记录。
### 5.3 Python 子图无 @RequirePermission 等价机制
- **data-ana**`shared/permissions.py` 定义了 `Permission` 常量与 `UserContext`,但
`schema.py` 中 Strawberry resolver 仅调用 `_get_user_context(info)` 提取用户身份,
未调用任何权限校验。Service 层使用 `DataScope` 做数据范围过滤,但不等价于权限点校验。
- **ai**`middleware/permission.py` 定义了 `require_permission` 装饰器,但面向 FastAPI 路由,
`kwargs["user_context"]` 提取上下文Strawberry resolver 用 `info: Info`,不兼容。
`Query.lesson_plan_status` / `Query.generated_report` 未声明 `info: Info` 参数。
**建议**Python 子图需引入 Strawberry middleware 或改造 `require_permission` 装饰器以支持
`info: Info`,属于独立基础设施工作,不在 Task 16 范围内。
### 5.4 iam DataScope 自定义内联守卫
iam `DataScopeResolver.dataScope` 查询未使用 `@RequirePermission`,但在 resolver 内部做了
`if (gqlCtx.userId !== userId && !gqlCtx.isAdmin) return null;` 的自助/管理员校验。
此模式适用于"用户查询自身数据"的自助场景,若改用 `@RequirePermission(IAM_USER_READ)` 反而
会阻止普通用户查询自己的 DataScope除非所有角色都授予 IAM_USER_READ
审计标记为 ✅Task 16 不修改。
---
## 6. 补齐计划
### 6.1 Task 16 范围内TS 子图19 个 resolver
为以下 19 个 TS resolver 追加 `@RequirePermission(...)` 装饰器,使用各子图既有 `Permissions` 常量:
| 子图 | resolver 文件 | 字段 | 权限点常量 |
| -------------- | ----------------------------- | ------------------ | ------------------------------------------ |
| iam | `user.resolver.ts` | user | `Permissions.IAM_USER_READ` |
| iam | `role.resolver.ts` | role | `Permissions.IAM_USER_READ` |
| config-service | `layout-template.resolver.ts` | layoutTemplates | `Permissions.CONFIG_USER` |
| config-service | `plugin-config.resolver.ts` | pluginConfig | `Permissions.CONFIG_USER` |
| config-service | `user-layout.resolver.ts` | userLayoutOverride | `Permissions.CONFIG_USER` |
| config-service | `plugin.resolver.ts` | plugin | `Permissions.CONFIG_USER` |
| config-service | `plugin.resolver.ts` | plugins | `Permissions.CONFIG_USER` |
| core-edu | `class.resolver.ts` | classInfo | `Permissions.CLASS_READ` |
| core-edu | `datascope.resolver.ts` | visibleGrades | `Permissions.GRADE_READ` |
| core-edu | `datascope.resolver.ts` | visibleExams | `Permissions.EXAM_READ` |
| core-edu | `exam.resolver.ts` | exam | `Permissions.EXAM_READ` |
| core-edu | `grade.resolver.ts` | grade | `Permissions.GRADE_READ` |
| core-edu | `homework.resolver.ts` | homework | `Permissions.HOMEWORK_READ` |
| content | `chapter.resolver.ts` | chapter | `Permissions.CONTENT_CHAPTER_READ` |
| content | `knowledge-point.resolver.ts` | knowledgePoint | `Permissions.CONTENT_KNOWLEDGE_POINT_READ` |
| content | `question.resolver.ts` | question | `Permissions.CONTENT_QUESTION_READ` |
| content | `textbook.resolver.ts` | textbook | `Permissions.CONTENT_TEXTBOOK_READ` |
| msg | `notification.resolver.ts` | notifications | `Permissions.MSG_NOTIFICATION_READ` |
| msg | `template.resolver.ts` | template | `Permissions.MSG_NOTIFICATION_MANAGE` |
### 6.2 Task 16 范围外Python 子图13 个 resolver需独立基础设施工作
- **data-ana**11 个):需在 `shared/permissions.py` 中为 Strawberry resolver 实现
`PermissionGuard` 类或 middleware`schema.py``@strawberry.field` 中调用
`_get_user_context(info)` 后执行 `guard.check(ctx, permission)`
- **ai**2 个):需改造 `middleware/permission.py``require_permission` 装饰器
以支持 Strawberry `info: Info`,并在 `schema.py``Query.lesson_plan_status` /
`Query.generated_report` 增加 `info: Info` 参数。
### 6.3 Follow-upAuthMiddleware 注册
为 iam / config-service / content / msg 的 `app.module.ts` 追加
`consumer.apply(AuthMiddleware).forRoutes("graphql")`,使 Task 16 补齐的
`@RequirePermission` 装饰器在生产环境(`DEV_MODE=false`)下能读取 `request.userId`/`userRoles`
该修改属 `app.module.ts`(非 resolver 文件),超出 Task 16 约束,作为独立 follow-up。
---
## 7. 验收
- [x] 审计范围覆盖 8 个子图
- [x] 审计方法可复现arch:scan + Grep + 逐文件 Read
- [x] 表格含子图 / Resolver / 字段 / 当前守卫 / 期望守卫 / 状态
- [x] 统计含总数 / 已有 / 缺失 / 覆盖率
- [x] 补齐计划指向 Task 16TS与 follow-upPython + AuthMiddleware
- [x] 关键发现记录 AuthMiddleware 覆盖差距与 Python 子图机制差异