import { apiError, type ApiErrorCode } from '@/lib/server/api-response'; import type { ClassroomGenerationJob, ClassroomJobOwnershipBinding, } from '@/lib/server/classroom-job-store'; import { classroomJobOwnership, observeClassroomJobAuthorization, } from '@/lib/server/authz/classroom-job-shadow'; import { evaluateResourceAuthorization, resolveIdentityEnforcementMode, type IdentityEnforcementMode, } from '@/lib/server/authz/identity-enforcement'; import { resolveRequestPrincipal, type PrincipalResolution, type RequestPrincipalResolver, } from '@/lib/server/authz/principal'; import { decideResourceAuthorization, type ResourceAuthorizationAction, type ResourceAuthorizationDecision, } from '@/lib/server/authz/resource-ownership'; import type { ResourceAuthorizationObserver } from '@/lib/server/authz/shadow-observer'; import { resourceOwnerBindingFromPrincipal } from '@/lib/server/authz/owner-binding'; export const CLASSROOM_JOB_MANAGE_ANY_CAPABILITY = 'classroom-job:manage:any'; export const CLASSROOM_JOB_CREATE_ANY_CAPABILITY = 'classroom-job:create:any'; export interface ClassroomJobAccessDeps { readonly mode?: IdentityEnforcementMode; readonly resolvePrincipal?: RequestPrincipalResolver; readonly observer?: ResourceAuthorizationObserver; } export interface ClassroomJobAccessDenial { readonly allowed: false; readonly status: number; readonly code: ApiErrorCode; readonly message: string; } export interface ClassroomJobAccessAllowed { readonly allowed: true; } export type ClassroomJobAccessResult = ClassroomJobAccessAllowed | ClassroomJobAccessDenial; export type ClassroomJobCreationAccessResult = | (ClassroomJobAccessAllowed & { readonly ownership: ClassroomJobOwnershipBinding }) | ClassroomJobAccessDenial; export type ClassroomJobListAccessResult = | (ClassroomJobAccessAllowed & { readonly jobs: ClassroomGenerationJob[] }) | ClassroomJobAccessDenial; function modeFrom(deps: ClassroomJobAccessDeps): IdentityEnforcementMode { return deps.mode ?? resolveIdentityEnforcementMode(); } async function resolvePrincipalSafely( request: Request, resolver: RequestPrincipalResolver | undefined, ): Promise { try { return await (resolver ?? resolveRequestPrincipal)(request); } catch { return { state: 'unavailable', reason: 'resolver_failed' }; } } function denialFor( principal: PrincipalResolution, decision?: ResourceAuthorizationDecision, ): ClassroomJobAccessDenial { if (principal.state === 'unavailable') { return { allowed: false, status: 503, code: 'IDENTITY_UNAVAILABLE', message: 'Identity verification is unavailable.', }; } if (principal.state === 'anonymous') { return { allowed: false, status: 401, code: 'AUTHENTICATION_REQUIRED', message: 'Authentication is required.', }; } if (decision?.effect === 'indeterminate') { return { allowed: false, status: 409, code: 'OWNERSHIP_MIGRATION_REQUIRED', message: 'This resource must be assigned to an account before it can be used.', }; } return { allowed: false, status: 403, code: 'FORBIDDEN', message: 'You do not have access to this resource.', }; } /** Resolve and bind the owner subject for a newly created public job. */ export async function authorizeClassroomJobCreation( request: Request, deps: ClassroomJobAccessDeps = {}, ): Promise { const principal = await resolvePrincipalSafely(request, deps.resolvePrincipal); const ownership: ClassroomJobOwnershipBinding = resourceOwnerBindingFromPrincipal(principal); if (modeFrom(deps) === 'shadow') { return { allowed: true, ownership }; } if (principal.state !== 'authenticated') return denialFor(principal); if (ownership.ownerPrincipalId || ownership.guestPrincipalId) { return { allowed: true, ownership }; } if (principal.principal.capabilities.includes(CLASSROOM_JOB_CREATE_ANY_CAPABILITY)) { return { allowed: true, ownership: {} }; } return denialFor(principal); } /** Enforce one job operation when required; preserve the former observer in shadow mode. */ export async function authorizeClassroomJobRequest( request: Request, job: ClassroomGenerationJob, action: ResourceAuthorizationAction, deps: ClassroomJobAccessDeps = {}, ): Promise { if (modeFrom(deps) === 'shadow') { await observeClassroomJobAuthorization(request, job, action, { ...(deps.resolvePrincipal ? { resolvePrincipal: deps.resolvePrincipal } : {}), ...(deps.observer ? { observer: deps.observer } : {}), }); return { allowed: true }; } const evaluation = await evaluateResourceAuthorization( { request, ownership: classroomJobOwnership(job), action, overrideCapability: CLASSROOM_JOB_MANAGE_ANY_CAPABILITY, }, deps.resolvePrincipal ? { resolvePrincipal: deps.resolvePrincipal } : {}, ); return evaluation.decision.effect === 'allow' ? { allowed: true } : denialFor(evaluation.principal, evaluation.decision); } /** Filter the global task list by resource ownership in required mode. */ export async function authorizeClassroomJobList( request: Request, jobs: ClassroomGenerationJob[], deps: ClassroomJobAccessDeps = {}, ): Promise { if (modeFrom(deps) === 'shadow') return { allowed: true, jobs }; const principal = await resolvePrincipalSafely(request, deps.resolvePrincipal); if (principal.state !== 'authenticated') return denialFor(principal); if (principal.principal.capabilities.includes(CLASSROOM_JOB_MANAGE_ANY_CAPABILITY)) { return { allowed: true, jobs }; } return { allowed: true, jobs: jobs.filter( (job) => decideResourceAuthorization({ principal, ownership: classroomJobOwnership(job), action: 'read', }).effect === 'allow', ), }; } export function classroomJobAccessError(result: ClassroomJobAccessDenial) { return apiError(result.code, result.status, result.message); }