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.
22 KiB
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. 审计方法
- 运行
pnpm run arch:scan更新 arch.db(TypeScript 20 模块 / 4801 符号、Go 2 模块 / 89 符号、Python 2 模块 / 559 符号、Protobuf 497 契约) - 通过 Grep 在
services/*/src/graphql/resolvers/*.resolver.ts与services/*/src/**/graphql/schema.py中搜索@Query、@Mutation、@ResolveField、@ResolveReference、@strawberry.field、resolve_reference等装饰器与字段 - 对每个 resolver 读取源码,核查是否具备以下任一守卫:
@RequirePermission(...)装饰器(TS 子图)- 自定义内联权限校验(如 dataScope 的 self/admin 检查)
- Federation
@ResolveReference()/resolve_reference(内部引用,不需守卫)
- 检查各子图
app.module.ts是否注册APP_GUARD全局PermissionGuard、RouterAuthGuard,以及AuthMiddleware是否覆盖/graphql路径 - 检查 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-AuthorizationHeader(生产环境仅允许 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装饰器 以支持 Strawberryinfo: 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. 验收
- 审计范围覆盖 8 个子图
- 审计方法可复现(arch:scan + Grep + 逐文件 Read)
- 表格含子图 / Resolver / 字段 / 当前守卫 / 期望守卫 / 状态
- 统计含总数 / 已有 / 缺失 / 覆盖率
- 补齐计划指向 Task 16(TS)与 follow-up(Python + AuthMiddleware)
- 关键发现记录 AuthMiddleware 覆盖差距与 Python 子图机制差异