wip league admin tools
This commit is contained in:
78
docs/league/actor-and-permissions.md
Normal file
78
docs/league/actor-and-permissions.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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).
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user