Files
gridpilot.gg/docs/league/actor-and-permissions.md
2025-12-28 12:04:12 +01:00

78 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 APIs 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).
---