From 1b5781bf425d592727237f0fa98d4b6505380e14 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:26:58 +0800 Subject: [PATCH] fix: add missing @RequirePermission decorators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds @RequirePermission to 19 TS GraphQL resolvers across 5 subgraphs (iam, config-service, core-edu, content, msg) per audit report §6.1. Maps: iam user/role -> IAM_USER_READ; config-service 5 queries -> CONFIG_USER; core-edu classInfo -> CLASS_READ, exam -> EXAM_READ, grade -> GRADE_READ, homework -> HOMEWORK_READ, datascope visibleGrades/visibleExams -> GRADE_READ/EXAM_READ; content chapter/knowledgePoint/question/ textbook -> CONTENT_*_READ; msg notifications -> MSG_NOTIFICATION_READ, template -> MSG_NOTIFICATION_MANAGE. Federation resolveReference left unguarded. Python subgraphs (data-ana, ai) deferred to follow-up infrastructure work. --- .../resolvers/layout-template.resolver.ts | 7 ++++++- .../resolvers/plugin-config.resolver.ts | 5 +++++ .../src/graphql/resolvers/plugin.resolver.ts | 6 ++++++ .../graphql/resolvers/user-layout.resolver.ts | 5 +++++ .../src/graphql/resolvers/chapter.resolver.ts | 7 ++++++- .../resolvers/knowledge-point.resolver.ts | 7 ++++++- .../graphql/resolvers/question.resolver.ts | 7 ++++++- .../graphql/resolvers/textbook.resolver.ts | 7 ++++++- .../src/graphql/resolvers/class.resolver.ts | 9 +++++++-- .../graphql/resolvers/datascope.resolver.ts | 20 ++++++++++++------- .../src/graphql/resolvers/exam.resolver.ts | 11 +++++++--- .../src/graphql/resolvers/grade.resolver.ts | 13 ++++++++---- .../graphql/resolvers/homework.resolver.ts | 7 ++++++- .../src/graphql/resolvers/role.resolver.ts | 7 ++++++- .../src/graphql/resolvers/user.resolver.ts | 5 +++++ .../resolvers/notification.resolver.ts | 19 +++++++++++------- .../graphql/resolvers/template.resolver.ts | 5 +++++ 17 files changed, 117 insertions(+), 30 deletions(-) diff --git a/services/config-service/src/graphql/resolvers/layout-template.resolver.ts b/services/config-service/src/graphql/resolvers/layout-template.resolver.ts index 616b1dc..9f6d505 100644 --- a/services/config-service/src/graphql/resolvers/layout-template.resolver.ts +++ b/services/config-service/src/graphql/resolvers/layout-template.resolver.ts @@ -6,6 +6,10 @@ */ import { Resolver, Query, ObjectType, Field, ID } from "@nestjs/graphql"; import { ConfigService } from "../../config-config/config.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; @ObjectType() export class LayoutTemplateGql { @@ -15,7 +19,7 @@ export class LayoutTemplateGql { @Field() displayName!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -27,6 +31,7 @@ export class LayoutTemplateResolver { constructor(private readonly service: ConfigService) {} @Query(() => [LayoutTemplateGql]) + @RequirePermission(Permissions.CONFIG_USER) async layoutTemplates(): Promise { const templates = await this.service.listLayoutTemplates(); return templates.map((t) => ({ diff --git a/services/config-service/src/graphql/resolvers/plugin-config.resolver.ts b/services/config-service/src/graphql/resolvers/plugin-config.resolver.ts index 06203de..4b7c84f 100644 --- a/services/config-service/src/graphql/resolvers/plugin-config.resolver.ts +++ b/services/config-service/src/graphql/resolvers/plugin-config.resolver.ts @@ -21,6 +21,10 @@ import { Int, } from "@nestjs/graphql"; import { ConfigService } from "../../config-config/config.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; @ObjectType() export class PluginConfigLayoutGql { @@ -123,6 +127,7 @@ export class PluginConfigResolver { * (来自 x-user-role 头),返回与 gRPC GetPluginConfig 等价的结果。 */ @Query(() => PluginConfigResponseGql) + @RequirePermission(Permissions.CONFIG_USER) async pluginConfig( @Args("userId", { type: () => ID }) userId: string, @Args("role", { diff --git a/services/config-service/src/graphql/resolvers/plugin.resolver.ts b/services/config-service/src/graphql/resolvers/plugin.resolver.ts index c68478e..7d946ff 100644 --- a/services/config-service/src/graphql/resolvers/plugin.resolver.ts +++ b/services/config-service/src/graphql/resolvers/plugin.resolver.ts @@ -21,6 +21,10 @@ import { type PluginRegistryEntity, } from "../dataloader.service.js"; import { ConfigService } from "../../config-config/config.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; @ObjectType() @Directive(`@key(fields: "pluginId")`) @@ -62,6 +66,7 @@ export class PluginResolver { } @Query(() => PluginRegistry, { nullable: true }) + @RequirePermission(Permissions.CONFIG_USER) async plugin( @Args("pluginId", { type: () => ID }) pluginId: string, ): Promise { @@ -69,6 +74,7 @@ export class PluginResolver { } @Query(() => [PluginRegistry]) + @RequirePermission(Permissions.CONFIG_USER) async plugins(): Promise { const list = await this.service.listPlugins({ isActive: true }); return list.map((p) => ({ diff --git a/services/config-service/src/graphql/resolvers/user-layout.resolver.ts b/services/config-service/src/graphql/resolvers/user-layout.resolver.ts index 1a55408..52b11e9 100644 --- a/services/config-service/src/graphql/resolvers/user-layout.resolver.ts +++ b/services/config-service/src/graphql/resolvers/user-layout.resolver.ts @@ -20,6 +20,10 @@ import { DataLoaderService, type UserLayoutOverrideEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; @ObjectType() @Directive(`@key(fields: "userId")`) @@ -46,6 +50,7 @@ export class UserLayoutResolver { } @Query(() => UserLayoutOverrideGql, { nullable: true }) + @RequirePermission(Permissions.CONFIG_USER) async userLayoutOverride( @Args("userId", { type: () => ID }) userId: string, ): Promise { diff --git a/services/content/src/graphql/resolvers/chapter.resolver.ts b/services/content/src/graphql/resolvers/chapter.resolver.ts index 5c4fa82..86bcd5e 100644 --- a/services/content/src/graphql/resolvers/chapter.resolver.ts +++ b/services/content/src/graphql/resolvers/chapter.resolver.ts @@ -19,6 +19,10 @@ import { DataLoaderService, type ChapterEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Chapter ObjectType(Federation @key) @@ -39,7 +43,7 @@ export class Chapter { @Field() order!: number; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) parentId: string | null = null; @Field() @@ -72,6 +76,7 @@ export class ChapterResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => Chapter, { nullable: true }) + @RequirePermission(Permissions.CONTENT_CHAPTER_READ) async chapter( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/content/src/graphql/resolvers/knowledge-point.resolver.ts b/services/content/src/graphql/resolvers/knowledge-point.resolver.ts index 93508fe..a136e26 100644 --- a/services/content/src/graphql/resolvers/knowledge-point.resolver.ts +++ b/services/content/src/graphql/resolvers/knowledge-point.resolver.ts @@ -19,6 +19,10 @@ import { DataLoaderService, type KnowledgePointEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * KnowledgePoint ObjectType(Federation @key) @@ -36,7 +40,7 @@ export class KnowledgePoint { @Field() title!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -71,6 +75,7 @@ export class KnowledgePointResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => KnowledgePoint, { nullable: true }) + @RequirePermission(Permissions.CONTENT_KNOWLEDGE_POINT_READ) async knowledgePoint( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/content/src/graphql/resolvers/question.resolver.ts b/services/content/src/graphql/resolvers/question.resolver.ts index d477932..a79f8e8 100644 --- a/services/content/src/graphql/resolvers/question.resolver.ts +++ b/services/content/src/graphql/resolvers/question.resolver.ts @@ -19,6 +19,10 @@ import { DataLoaderService, type QuestionEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Question ObjectType(Federation @key) @@ -42,7 +46,7 @@ export class Question { @Field() answer!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) explanation: string | null = null; @Field() @@ -84,6 +88,7 @@ export class QuestionResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => Question, { nullable: true }) + @RequirePermission(Permissions.CONTENT_QUESTION_READ) async question( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/content/src/graphql/resolvers/textbook.resolver.ts b/services/content/src/graphql/resolvers/textbook.resolver.ts index 99c59cf..ad24712 100644 --- a/services/content/src/graphql/resolvers/textbook.resolver.ts +++ b/services/content/src/graphql/resolvers/textbook.resolver.ts @@ -20,6 +20,10 @@ import { DataLoaderService, type TextbookEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Textbook ObjectType(Federation @key) @@ -46,7 +50,7 @@ export class Textbook { @Field() status!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) tenantId: string | null = null; @Field() @@ -76,6 +80,7 @@ export class TextbookResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => Textbook, { nullable: true }) + @RequirePermission(Permissions.CONTENT_TEXTBOOK_READ) async textbook( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/core-edu/src/graphql/resolvers/class.resolver.ts b/services/core-edu/src/graphql/resolvers/class.resolver.ts index b543948..0270444 100644 --- a/services/core-edu/src/graphql/resolvers/class.resolver.ts +++ b/services/core-edu/src/graphql/resolvers/class.resolver.ts @@ -21,6 +21,10 @@ import { type ClassEntity, type StudentEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * ClassInfo ObjectType(Federation @key) @@ -38,10 +42,10 @@ export class ClassInfo { @Field() gradeId!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) headTeacherId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -78,6 +82,7 @@ export class ClassResolver { } @Query(() => ClassInfo, { nullable: true }) + @RequirePermission(Permissions.CLASS_READ) async classInfo( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/core-edu/src/graphql/resolvers/datascope.resolver.ts b/services/core-edu/src/graphql/resolvers/datascope.resolver.ts index 9ccad0f..32f5f8e 100644 --- a/services/core-edu/src/graphql/resolvers/datascope.resolver.ts +++ b/services/core-edu/src/graphql/resolvers/datascope.resolver.ts @@ -26,6 +26,10 @@ import { import type { RedisClientType } from "redis"; import { ExamsRepository } from "../../exams/exams.repository.js"; import { GradesRepository } from "../../grades/grades.repository.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * ScopedGrade 引用类型(仅用于 DataScope 返回) @@ -40,10 +44,10 @@ class ScopedGrade { @Field() studentId!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) examId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) homeworkId: string | null = null; @Field() @@ -52,7 +56,7 @@ class ScopedGrade { @Field() totalScore!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) feedback: string | null = null; @Field() @@ -61,7 +65,7 @@ class ScopedGrade { @Field() schoolId!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) idempotencyKey: string | null = null; @Field() @@ -89,7 +93,7 @@ class ScopedExam { @Field() title!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -107,7 +111,7 @@ class ScopedExam { @Field() statusChangedAt!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) statusChangedBy: string | null = null; @Field() @@ -116,7 +120,7 @@ class ScopedExam { @Field() createdBy!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) archivedAt: string | null = null; @Field() @@ -227,6 +231,7 @@ export class DataScopeResolver { * Router 通过 @requires 将 studentScopeToken 传入 parent。 */ @ResolveField(() => [ScopedGrade]) + @RequirePermission(Permissions.GRADE_READ) async visibleGrades( @Parent() parent: { userId: string; studentScopeToken: string }, ): Promise { @@ -250,6 +255,7 @@ export class DataScopeResolver { * visibleExams:通过 classScopeToken 解析可见班级 ID,查询考试 */ @ResolveField(() => [ScopedExam]) + @RequirePermission(Permissions.EXAM_READ) async visibleExams( @Parent() parent: { userId: string; classScopeToken: string }, ): Promise { diff --git a/services/core-edu/src/graphql/resolvers/exam.resolver.ts b/services/core-edu/src/graphql/resolvers/exam.resolver.ts index 5336a9b..b582d28 100644 --- a/services/core-edu/src/graphql/resolvers/exam.resolver.ts +++ b/services/core-edu/src/graphql/resolvers/exam.resolver.ts @@ -17,6 +17,10 @@ import { Directive, } from "@nestjs/graphql"; import { DataLoaderService, type ExamEntity } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Exam ObjectType(Federation @key) @@ -37,7 +41,7 @@ export class Exam { @Field() title!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -55,7 +59,7 @@ export class Exam { @Field() statusChangedAt!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) statusChangedBy: string | null = null; @Field() @@ -64,7 +68,7 @@ export class Exam { @Field() createdBy!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) archivedAt: string | null = null; @Field() @@ -94,6 +98,7 @@ export class ExamResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => Exam, { nullable: true }) + @RequirePermission(Permissions.EXAM_READ) async exam( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/core-edu/src/graphql/resolvers/grade.resolver.ts b/services/core-edu/src/graphql/resolvers/grade.resolver.ts index 132e231..73884f8 100644 --- a/services/core-edu/src/graphql/resolvers/grade.resolver.ts +++ b/services/core-edu/src/graphql/resolvers/grade.resolver.ts @@ -16,6 +16,10 @@ import { Directive, } from "@nestjs/graphql"; import { DataLoaderService, type GradeEntity } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Grade ObjectType(Federation @key) @@ -30,10 +34,10 @@ export class Grade { @Field() studentId!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) examId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) homeworkId: string | null = null; @Field() @@ -42,7 +46,7 @@ export class Grade { @Field() totalScore!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) feedback: string | null = null; @Field() @@ -51,7 +55,7 @@ export class Grade { @Field() schoolId!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) idempotencyKey: string | null = null; @Field() @@ -71,6 +75,7 @@ export class GradeResolver { } @Query(() => Grade, { nullable: true }) + @RequirePermission(Permissions.GRADE_READ) async grade( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/core-edu/src/graphql/resolvers/homework.resolver.ts b/services/core-edu/src/graphql/resolvers/homework.resolver.ts index 1b06162..75293a6 100644 --- a/services/core-edu/src/graphql/resolvers/homework.resolver.ts +++ b/services/core-edu/src/graphql/resolvers/homework.resolver.ts @@ -19,6 +19,10 @@ import { DataLoaderService, type HomeworkEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Homework ObjectType(Federation @key) @@ -39,7 +43,7 @@ export class Homework { @Field() title!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -74,6 +78,7 @@ export class HomeworkResolver { } @Query(() => Homework, { nullable: true }) + @RequirePermission(Permissions.HOMEWORK_READ) async homework( @Args("id", { type: () => ID }) id: string, ): Promise { diff --git a/services/iam/src/graphql/resolvers/role.resolver.ts b/services/iam/src/graphql/resolvers/role.resolver.ts index 6e3560e..fb481f4 100644 --- a/services/iam/src/graphql/resolvers/role.resolver.ts +++ b/services/iam/src/graphql/resolvers/role.resolver.ts @@ -16,6 +16,10 @@ import { Directive, } from "@nestjs/graphql"; import { DataLoaderService, type RoleEntity } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; @ObjectType() @Directive(`@key(fields: "roleId")`) @@ -26,7 +30,7 @@ export class Role { @Field() name!: string; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) description: string | null = null; @Field() @@ -46,6 +50,7 @@ export class RoleResolver { } @Query(() => Role, { nullable: true }) + @RequirePermission(Permissions.IAM_USER_READ) async role( @Args("roleId", { type: () => ID }) roleId: string, ): Promise { diff --git a/services/iam/src/graphql/resolvers/user.resolver.ts b/services/iam/src/graphql/resolvers/user.resolver.ts index 22fcabd..79c2d12 100644 --- a/services/iam/src/graphql/resolvers/user.resolver.ts +++ b/services/iam/src/graphql/resolvers/user.resolver.ts @@ -17,6 +17,10 @@ import { Directive, } from "@nestjs/graphql"; import { DataLoaderService, type UserEntity } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * User ObjectType(Federation @key) @@ -61,6 +65,7 @@ export class UserResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => User, { nullable: true }) + @RequirePermission(Permissions.IAM_USER_READ) async user( @Args("userId", { type: () => ID }) userId: string, ): Promise { diff --git a/services/msg/src/graphql/resolvers/notification.resolver.ts b/services/msg/src/graphql/resolvers/notification.resolver.ts index 0877ee6..b1c0bc5 100644 --- a/services/msg/src/graphql/resolvers/notification.resolver.ts +++ b/services/msg/src/graphql/resolvers/notification.resolver.ts @@ -21,6 +21,10 @@ import { type NotificationEntity, } from "../dataloader.service.js"; import { listByUser } from "../../notifications/notifications.repository.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * Notification ObjectType(Federation @key) @@ -59,25 +63,25 @@ export class Notification { @Field() updatedAt!: Date; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) relatedEntityType: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) relatedEntityId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) groupId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) senderId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) templateId: string | null = null; - @Field({ nullable: true }) + @Field(() => String, { nullable: true }) eventId: string | null = null; - @Field({ nullable: true }) + @Field(() => Date, { nullable: true }) readAt: Date | null = null; } @@ -103,6 +107,7 @@ export class NotificationResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => [Notification]) + @RequirePermission(Permissions.MSG_NOTIFICATION_READ) async notifications( @Args("userId", { type: () => ID }) userId: string, ): Promise { diff --git a/services/msg/src/graphql/resolvers/template.resolver.ts b/services/msg/src/graphql/resolvers/template.resolver.ts index 526233f..d2b1262 100644 --- a/services/msg/src/graphql/resolvers/template.resolver.ts +++ b/services/msg/src/graphql/resolvers/template.resolver.ts @@ -19,6 +19,10 @@ import { DataLoaderService, type TemplateEntity, } from "../dataloader.service.js"; +import { + Permissions, + RequirePermission, +} from "../../middleware/permission.guard.js"; /** * NotificationTemplate ObjectType(Federation @key) @@ -81,6 +85,7 @@ export class TemplateResolver { * 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝(ADR-036) */ @Query(() => NotificationTemplate, { nullable: true }) + @RequirePermission(Permissions.MSG_NOTIFICATION_MANAGE) async template( @Args("id", { type: () => ID }) id: string, ): Promise {