fix: add missing @RequirePermission decorators
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.
This commit is contained in:
@@ -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<LayoutTemplateGql[]> {
|
||||
const templates = await this.service.listLayoutTemplates();
|
||||
return templates.map((t) => ({
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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<PluginRegistryEntity | null> {
|
||||
@@ -69,6 +74,7 @@ export class PluginResolver {
|
||||
}
|
||||
|
||||
@Query(() => [PluginRegistry])
|
||||
@RequirePermission(Permissions.CONFIG_USER)
|
||||
async plugins(): Promise<PluginRegistryEntity[]> {
|
||||
const list = await this.service.listPlugins({ isActive: true });
|
||||
return list.map((p) => ({
|
||||
|
||||
@@ -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<UserLayoutOverrideEntity | null> {
|
||||
|
||||
@@ -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<ChapterEntity | null> {
|
||||
|
||||
@@ -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<KnowledgePointEntity | null> {
|
||||
|
||||
@@ -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<QuestionEntity | null> {
|
||||
|
||||
@@ -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<TextbookEntity | null> {
|
||||
|
||||
@@ -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<ClassEntity | null> {
|
||||
|
||||
@@ -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<ScopedGrade[]> {
|
||||
@@ -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<ScopedExam[]> {
|
||||
|
||||
@@ -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<ExamEntity | null> {
|
||||
|
||||
@@ -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<GradeEntity | null> {
|
||||
|
||||
@@ -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<HomeworkEntity | null> {
|
||||
|
||||
@@ -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<RoleEntity | null> {
|
||||
|
||||
@@ -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<UserEntity | null> {
|
||||
|
||||
@@ -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<NotificationEntity[]> {
|
||||
|
||||
@@ -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<TemplateEntity | null> {
|
||||
|
||||
Reference in New Issue
Block a user