import { apiError, type ApiErrorCode } from '@/lib/server/api-response'; import type { PersistClassroomOwnership, PersistedClassroomData, } from '@/lib/server/classroom-storage'; import { evaluateResourceAuthorization, resolveIdentityEnforcementMode, type IdentityEnforcementMode, } from '@/lib/server/authz/identity-enforcement'; import { resolveRequestPrincipal, type PrincipalResolution, type RequestPrincipalResolver, } from '@/lib/server/authz/principal'; import { resourceOwnerBindingFromPrincipal } from '@/lib/server/authz/owner-binding'; import type { ResourceAuthorizationAction } from '@/lib/server/authz/resource-ownership'; export const CLASSROOM_MANAGE_ANY_CAPABILITY = 'classroom:manage:any'; export const CLASSROOM_CREATE_ANY_CAPABILITY = 'classroom:create:any'; export interface ClassroomAccessDeps { readonly mode?: IdentityEnforcementMode; readonly resolvePrincipal?: RequestPrincipalResolver; } export interface ClassroomAccessDenial { readonly allowed: false; readonly status: number; readonly code: ApiErrorCode; readonly message: string; } export interface ClassroomAccessAllowed { readonly allowed: true; } export type ClassroomAccessResult = ClassroomAccessAllowed | ClassroomAccessDenial; export type ClassroomCreationAccessResult = | (ClassroomAccessAllowed & { readonly ownership: PersistClassroomOwnership }) | ClassroomAccessDenial; function classroomOwnership(classroom: PersistedClassroomData) { const hasOwner = Object.prototype.hasOwnProperty.call(classroom, 'ownerPrincipalId'); const hasGuest = Object.prototype.hasOwnProperty.call(classroom, 'guestPrincipalId'); if (hasOwner && hasGuest) return { state: 'unknown' as const }; if (hasOwner) { return { state: 'owned' as const, ownerPrincipalId: classroom.ownerPrincipalId ?? '' }; } if (hasGuest) { return { state: 'guest-owned' as const, guestPrincipalId: classroom.guestPrincipalId ?? '' }; } return { state: 'legacy-unowned' as const }; } async function resolvePrincipalSafely( request: Request, resolver?: RequestPrincipalResolver, ): Promise { try { return await (resolver ?? resolveRequestPrincipal)(request); } catch { return { state: 'unavailable', reason: 'resolver_failed' }; } } function denialFor(principal: PrincipalResolution, indeterminate = false): ClassroomAccessDenial { 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 (indeterminate) { return { allowed: false, status: 409, code: 'OWNERSHIP_MIGRATION_REQUIRED', message: 'This classroom 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 classroom.', }; } /** Resolve the server-side owner binding for a newly persisted classroom. */ export async function authorizeClassroomCreation( request: Request, deps: ClassroomAccessDeps = {}, ): Promise { const principal = await resolvePrincipalSafely(request, deps.resolvePrincipal); const ownership = resourceOwnerBindingFromPrincipal(principal); const mode = deps.mode ?? resolveIdentityEnforcementMode(); if (mode === '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_CREATE_ANY_CAPABILITY)) { return { allowed: true, ownership: {} }; } return denialFor(principal); } /** Enforce access to an existing raw classroom when identity is required. */ export async function authorizeClassroomRequest( request: Request, classroom: PersistedClassroomData, action: ResourceAuthorizationAction, deps: ClassroomAccessDeps = {}, ): Promise { if ((deps.mode ?? resolveIdentityEnforcementMode()) === 'shadow') { return { allowed: true }; } const evaluation = await evaluateResourceAuthorization( { request, ownership: classroomOwnership(classroom), action, overrideCapability: CLASSROOM_MANAGE_ANY_CAPABILITY, }, deps.resolvePrincipal ? { resolvePrincipal: deps.resolvePrincipal } : {}, ); return evaluation.decision.effect === 'allow' ? { allowed: true } : denialFor(evaluation.principal, evaluation.decision.effect === 'indeterminate'); } export function classroomAccessError(result: ClassroomAccessDenial) { return apiError(result.code, result.status, result.message); }