From 315b9549981c38d2654e82cbb9528c19043fef09 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:20:30 +0800 Subject: [PATCH] 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. --- docs/security/graphql-auth-audit-2026-07.md | 294 ++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 docs/security/graphql-auth-audit-2026-07.md diff --git a/docs/security/graphql-auth-audit-2026-07.md b/docs/security/graphql-auth-audit-2026-07.md new file mode 100644 index 0000000..7c945e0 --- /dev/null +++ b/docs/security/graphql-auth-audit-2026-07.md @@ -0,0 +1,294 @@ +# 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.db(TypeScript 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 GraphQL(Python),不使用 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 resolveReference(10)+ 自定义内联守卫(1)+ 面向用户字段(39) | +| 已有守卫 | 18 | 10 resolveReference + 1 iam.dataScope 自定义内联 + 7(Python 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-in(TS 子图) + +所有 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-up:AuthMiddleware 注册 + +为 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 16(TS)与 follow-up(Python + AuthMiddleware) +- [x] 关键发现记录 AuthMiddleware 覆盖差距与 Python 子图机制差异