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

22 KiB
Raw Blame History

GraphQL 字段级 @auth 审计报告2026-07

版本v1.0 日期2026-07-17 范围portal-shell 数据抽象层与 GraphQL 安全加固 plan Task 15 关联:


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.tsservices/*/src/**/graphql/schema.py 中搜索 @Query@Mutation@ResolveField@ResolveReference@strawberry.fieldresolve_reference 等装饰器与字段
  3. 对每个 resolver 读取源码,核查是否具备以下任一守卫:
    • @RequirePermission(...) 装饰器TS 子图)
    • 自定义内联权限校验(如 dataScope 的 self/admin 检查)
    • Federation @ResolveReference() / resolve_reference(内部引用,不需守卫)
  4. 检查各子图 app.module.ts 是否注册 APP_GUARD 全局 PermissionGuardRouterAuthGuard,以及 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 resolverinfo: 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 PermissionGuardRouterAuthGuard

  • RouterAuthGuard:校验 Router-Authorization Header生产环境仅允许 Apollo Router 调用子图)
  • PermissionGuardopt-in 模式——无 @RequirePermission() 装饰器时直接 return true 不会自动拒绝未声明权限点的 resolver

因此,全局 Guard 的存在不等于字段级守卫全覆盖。本次审计将所有未显式声明 @RequirePermission 的面向用户 resolver 标记为 Task 16 会逐个补齐。

5.2 AuthMiddleware 未覆盖 /graphql重要

PermissionGuard 依赖 request.userId / request.userRoles,这两个字段由 AuthMiddlewarex-user-id / x-user-roles Header 注入。但各子图 app.module.tsAuthMiddleware 的注册范围不一致:

子图 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-anashared/permissions.py 定义了 Permission 常量与 UserContext,但 schema.py 中 Strawberry resolver 仅调用 _get_user_context(info) 提取用户身份, 未调用任何权限校验。Service 层使用 DataScope 做数据范围过滤,但不等价于权限点校验。
  • aimiddleware/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-ana11 个):需在 shared/permissions.py 中为 Strawberry resolver 实现 PermissionGuard 类或 middlewareschema.py@strawberry.field 中调用 _get_user_context(info) 后执行 guard.check(ctx, permission)
  • ai2 个):需改造 middleware/permission.pyrequire_permission 装饰器 以支持 Strawberry info: Info,并在 schema.pyQuery.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. 验收

  • 审计范围覆盖 8 个子图
  • 审计方法可复现arch:scan + Grep + 逐文件 Read
  • 表格含子图 / Resolver / 字段 / 当前守卫 / 期望守卫 / 状态
  • 统计含总数 / 已有 / 缺失 / 覆盖率
  • 补齐计划指向 Task 16TS与 follow-upPython + AuthMiddleware
  • 关键发现记录 AuthMiddleware 覆盖差距与 Python 子图机制差异