feat(teacher-bff): 完整实现 teacher-bff GraphQL 聚合层
包含 clients/graphql/middleware、health probes、shared-ts contracts 等
This commit is contained in:
123
services/teacher-bff/src/middleware/authorization.guard.ts
Normal file
123
services/teacher-bff/src/middleware/authorization.guard.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// AuthorizationGuard 越权防御(B4 + president §2.7/§2.9)
|
||||
// P2 实现:DEV_MODE 放行 + 日志告警;生产拒绝(保守策略)
|
||||
// P3+ 替换为真实 gRPC 校验(core-edu.GetClassesByTeacher 比对 teacherId)
|
||||
//
|
||||
// 3 类越权错误码(president §2.7):
|
||||
// - BFF_TEACHER_UNAUTHORIZED (401): x-user-id 缺失或无效
|
||||
// - BFF_TEACHER_FORBIDDEN_RESOURCE (403): teacherId 与资源无归属关系(场景 A)
|
||||
// - BFF_TEACHER_IDENTITY_MISMATCH (403): JWT teacherId 与 body teacherId 不一致(场景 B)
|
||||
import { Injectable, Inject } from "@nestjs/common";
|
||||
import { env } from "../config/env.js";
|
||||
import { logger } from "../shared/observability/logger.js";
|
||||
import {
|
||||
ForbiddenResourceError,
|
||||
IdentityMismatchError,
|
||||
} from "../shared/errors/application-error.js";
|
||||
import { IAM_CLIENT } from "../clients/iam/iam-client.interface.js";
|
||||
import type { IamClient } from "../clients/iam/iam-client.interface.js";
|
||||
|
||||
/**
|
||||
* AuthorizationGuard 接口(president §2.9)
|
||||
* P2: DEV_MODE 放行;P3+: 真实 gRPC 校验
|
||||
*/
|
||||
export interface AuthorizationGuard {
|
||||
/** 校验教师是否有权访问指定班级(场景 A:teacherId 与资源归属) */
|
||||
canAccessClass(userId: string, classId: string): Promise<boolean>;
|
||||
|
||||
/** 校验教师是否有权访问指定考试 */
|
||||
canAccessExam(userId: string, examId: string): Promise<boolean>;
|
||||
|
||||
/** 校验教师是否有权访问指定作业 */
|
||||
canAccessHomework(userId: string, homeworkId: string): Promise<boolean>;
|
||||
|
||||
/** 校验身份一致性(场景 B:JWT teacherId 与 body teacherId) */
|
||||
assertIdentityMatch(jwtTeacherId: string, bodyTeacherId: string): void;
|
||||
}
|
||||
|
||||
export const AUTHORIZATION_GUARD = Symbol("AUTHORIZATION_GUARD");
|
||||
|
||||
/**
|
||||
* P2 实现:DEV_MODE 放行 + 日志告警
|
||||
* 生产环境(DEV_MODE=false)对所有资源校验返回 false(保守拒绝)
|
||||
* P3+ 替换为 AuthorizationGuardGrpcImpl(接入 core-edu gRPC 真实校验)
|
||||
*/
|
||||
@Injectable()
|
||||
export class AuthorizationGuardImpl implements AuthorizationGuard {
|
||||
constructor(@Inject(IAM_CLIENT) private readonly iam: IamClient) {}
|
||||
|
||||
async canAccessClass(userId: string, classId: string): Promise<boolean> {
|
||||
if (env.TEACHER_BFF_DEV_MODE) {
|
||||
logger.warn(
|
||||
{ userId, classId, devMode: true },
|
||||
"AuthorizationGuard DEV_MODE: access allowed without real check",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// P2 生产模式:保守拒绝(P3+ 接入 core-edu.GetClassesByTeacher 真实校验)
|
||||
logger.error(
|
||||
{ userId, classId, devMode: false },
|
||||
"AuthorizationGuard production mode not implemented yet (P3+)",
|
||||
);
|
||||
throw new ForbiddenResourceError(
|
||||
`User ${userId} has no access to class ${classId}`,
|
||||
{ userId, classId, reason: "production_check_not_implemented" },
|
||||
);
|
||||
}
|
||||
|
||||
async canAccessExam(userId: string, examId: string): Promise<boolean> {
|
||||
if (env.TEACHER_BFF_DEV_MODE) {
|
||||
logger.warn(
|
||||
{ userId, examId, devMode: true },
|
||||
"AuthorizationGuard DEV_MODE: access allowed without real check",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
logger.error(
|
||||
{ userId, examId, devMode: false },
|
||||
"AuthorizationGuard production mode not implemented yet (P3+)",
|
||||
);
|
||||
throw new ForbiddenResourceError(
|
||||
`User ${userId} has no access to exam ${examId}`,
|
||||
{ userId, examId, reason: "production_check_not_implemented" },
|
||||
);
|
||||
}
|
||||
|
||||
async canAccessHomework(
|
||||
userId: string,
|
||||
homeworkId: string,
|
||||
): Promise<boolean> {
|
||||
if (env.TEACHER_BFF_DEV_MODE) {
|
||||
logger.warn(
|
||||
{ userId, homeworkId, devMode: true },
|
||||
"AuthorizationGuard DEV_MODE: access allowed without real check",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
logger.error(
|
||||
{ userId, homeworkId, devMode: false },
|
||||
"AuthorizationGuard production mode not implemented yet (P3+)",
|
||||
);
|
||||
throw new ForbiddenResourceError(
|
||||
`User ${userId} has no access to homework ${homeworkId}`,
|
||||
{ userId, homeworkId, reason: "production_check_not_implemented" },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验身份一致性(场景 B,president §2.7)
|
||||
* JWT 中的 teacherId 必须与请求 body 中的 teacherId 一致
|
||||
* 此校验在所有模式(DEV_MODE / 生产)下都强制执行
|
||||
*/
|
||||
assertIdentityMatch(jwtTeacherId: string, bodyTeacherId: string): void {
|
||||
if (jwtTeacherId !== bodyTeacherId) {
|
||||
logger.error(
|
||||
{ jwtTeacherId, bodyTeacherId },
|
||||
"Identity mismatch: JWT teacherId != body teacherId",
|
||||
);
|
||||
throw new IdentityMismatchError(
|
||||
`JWT teacherId (${jwtTeacherId}) does not match body teacherId (${bodyTeacherId})`,
|
||||
{ jwtTeacherId, bodyTeacherId },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
services/teacher-bff/src/middleware/middleware.module.ts
Normal file
19
services/teacher-bff/src/middleware/middleware.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// 中间件模块(AuthorizationGuard 越权防御,B4 + president §2.9)
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ClientsModule } from "../clients/clients.module.js";
|
||||
import {
|
||||
AUTHORIZATION_GUARD,
|
||||
AuthorizationGuardImpl,
|
||||
} from "./authorization.guard.js";
|
||||
|
||||
@Module({
|
||||
imports: [ClientsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: AUTHORIZATION_GUARD,
|
||||
useClass: AuthorizationGuardImpl,
|
||||
},
|
||||
],
|
||||
exports: [AUTHORIZATION_GUARD],
|
||||
})
|
||||
export class MiddlewareModule {}
|
||||
Reference in New Issue
Block a user