# League Actor Model & Permissions (Canonical) This document defines the canonical backend actor model and the permission rules for **league admin/owner** operations. It is the source of truth for Subtask 0A in [`plans/league-admin-mvp-plan.md`](plans/league-admin-mvp-plan.md:1). --- ## Session identity (source of truth) ### What the authenticated session contains - The API authentication layer attaches `request.user.userId` based on the session cookie (`gp_session`). - See [`AuthenticationGuard.canActivate()`](apps/api/src/domain/auth/AuthenticationGuard.ts:16). - The backend uses an async request context (`AsyncLocalStorage`) to make the current request available to services. - See [`requestContextMiddleware()`](adapters/http/RequestContext.ts:24). - Wired globally for the API via [`AppModule.configure()`](apps/api/src/app.module.ts:49). ### Mapping: `userId` → `driverId` Current canonical mapping (for MVP): - The “actor” is derived from session, and `driverId === userId`. - This is implemented by [`getActorFromRequestContext()`](apps/api/src/domain/auth/getActorFromRequestContext.ts:12). Rationale: - The current system uses the session user identity as the same identifier used by racing/league membership repositories (e.g. seeded admin user is `driver-1` in session). - If/when we introduce a real user ↔ driver relationship (1:N), this function becomes the single authoritative mapping point. --- ## Canonical actor model The API’s canonical “actor” is: ```ts type Actor = { userId: string; driverId: string }; ``` Returned by [`getActorFromRequestContext()`](apps/api/src/domain/auth/getActorFromRequestContext.ts:12). Rules: - All auth/permissions decisions use the actor derived from the authenticated session. - Controllers and services must never use request-body “performer/admin IDs” for authorization decisions. --- ## League permissions: admin/owner ### Meaning of “league admin/owner” A driver is authorized as a league admin if: - They have an **active** membership in the league, and - Their membership role is either `owner` or `admin`. Authoritative check: - Implemented in the core use case [`GetLeagueAdminPermissionsUseCase.execute()`](core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase.ts:39) by loading membership and validating `status` + `role`. ### How it is validated server-side Canonical enforcement entrypoint (API layer): - [`requireLeagueAdminOrOwner()`](apps/api/src/domain/league/LeagueAuthorization.ts:15) This helper: - Derives the actor from session via [`getActorFromRequestContext()`](apps/api/src/domain/auth/getActorFromRequestContext.ts:12) - Invokes the core use case with `performerDriverId: actor.driverId` --- ## Contract rule (non-negotiable) **No league write operation may accept performer/admin IDs for auth decisions.** Concretely: - Request DTOs may still temporarily contain IDs for “target entities” (e.g. `targetDriverId`), but never the acting user/admin/performer ID. - Any endpoint/service that needs “who is performing this” MUST obtain it from session-derived actor, not from request payload, params, or hardcoded values. Tests: - Actor derives from session, not payload: [`ActorFromSession`](apps/api/src/domain/auth/ActorFromSession.test.ts:17). - Permission helper uses session-derived actor consistently: [`ActorFromSession`](apps/api/src/domain/auth/ActorFromSession.test.ts:30). - Example application in a league write-like operation (`joinLeague`) ignores payload driverId and uses session actor: [`LeagueService`](apps/api/src/domain/league/LeagueService.test.ts:1). ---