# 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 子图机制差异