Files
gridpilot.gg/docs/architecture/api/AUTHORIZATION.md
2026-01-11 14:42:54 +01:00

257 lines
7.9 KiB
Markdown
Raw Permalink 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.
# Authorization (Roles + Permissions)
This document defines the **authorization concept** for GridPilot, based on a clear role taxonomy and a permission-first model that scales to:
- system/global admins
- league-scoped admins/stewards
- sponsor-scoped admins
- team-scoped admins
- future “super admin” tooling
It complements (but does not replace) feature availability:
- Feature availability answers: “Is this capability enabled at all?”
- Authorization answers: “Is this actor allowed to do it?”
Related:
- Feature gating concept: [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1)
---
## 1) Terms
### 1.1 Actor
The authenticated user performing a request.
### 1.2 Resource Scope
A resource boundary that defines where a role applies:
- **system**: global platform scope
- **league**: role applies only inside a league
- **sponsor**: role applies only inside a sponsor account
- **team**: role applies only inside a team
### 1.3 Permission
A normalized action on a capability, expressed as:
- `capabilityKey`
- `actionType` (`view` or `mutate`)
Examples:
- `league.admin.members` + `mutate`
- `league.stewarding.protests` + `view`
- `sponsors.portal` + `view`
---
## 2) Role Taxonomy (Canonical)
These are the roles you described, organized by scope.
### 2.1 System Roles (global)
- `owner`
Highest authority. Intended for a tiny set of internal operators.
- `admin`
Platform admin. Can manage most platform features.
### 2.2 League Roles (scoped to a leagueId)
- `league_owner`
Full control over that league.
- `league_admin`
Admin control over that league.
- `league_steward`
Stewarding workflow privileges (protests, penalties, reviews), plus any explicitly granted admin powers.
### 2.3 Sponsor Roles (scoped to a sponsorId)
- `sponsor_owner`
Full control over that sponsor account.
- `sponsor_admin`
Admin control for sponsor account operations.
### 2.4 Team Roles (scoped to a teamId)
- `team_owner`
Full control over that team.
- `team_admin`
Admin control for team operations.
### 2.5 Default Role
- `user`
Every authenticated account has this implicitly.
Notes:
- “Role” is an access label; it is not a separate identity type. Admins, drivers, team captains are still “users”.
---
## 3) Role Composition Rules
Authorization is evaluated with **role composition**:
1) **System roles** apply everywhere.
2) **Scoped roles** apply only when the request targets that scope.
Examples:
- A user can be `league_admin` in League A and just `user` in League B.
- A system `admin` is allowed even without scoped roles (unless an endpoint explicitly requires scoped membership).
---
## 4) Permission-First Model (Recommended)
Instead of scattering checks like “is admin?” across controllers/services, define:
- a small, stable set of permissions (capabilityKey + actionType)
- a role → permission mapping table
- membership resolvers that answer: “what scoped roles does this actor have for this resourceId?”
### 4.1 Why permission-first
- Centralizes security logic
- Makes audit/review simpler
- Avoids “new endpoint forgot a check”
- Enables future super-admin tooling by manipulating roles/permissions cleanly
---
## 5) Default Access Policy (Protect All Endpoints)
To properly “protect all endpoints”, the platform must move to:
### 5.1 Deny-by-default
- Every API route requires an authenticated actor **unless explicitly marked public**.
### 5.2 Explicit public routes
A route is public only when explicitly marked as such (conceptually “Public metadata”).
This prevents “we forgot to add guards” from becoming a security issue.
### 5.3 Actor identity must not be caller-controlled
Any endpoint that currently accepts identifiers like:
- `performerDriverId`
- `adminId`
- `stewardId`
must stop trusting those fields and derive the actor identity from the authenticated session.
---
## 6) 403 vs 404 (Non-Disclosure Rules)
Use different status codes for different security goals:
### 6.1 Forbidden (403)
Return **403** when:
- the resource exists
- the actor is authenticated
- the actor lacks permission
This is the normal authorization failure.
### 6.2 Not Found (404) for non-disclosure
Return **404** when:
- revealing the existence of the resource would leak sensitive information
- the route is explicitly designated “non-disclosing”
Use this sparingly and intentionally.
### 6.3 Feature availability interaction
Feature availability failures (disabled/hidden/coming soon) should behave as “not found” for public callers, while maintenance mode should return 503. See [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1).
---
## 7) Suggested Role → Permission Mapping (First Pass)
This table is a starting point (refine as product scope increases).
### 7.1 System
- `owner`: all permissions
- `admin`: platform-admin permissions (payments admin, sponsor portal admin, moderation)
### 7.2 League
- `league_owner`: all league permissions for that league
- `league_admin`: league management permissions (members, config, seasons, schedule, wallet)
- `league_steward`: stewarding permissions (review protests, apply penalties), and optionally limited admin view permissions
### 7.3 Sponsor
- `sponsor_owner`: all sponsor permissions for that sponsor
- `sponsor_admin`: sponsor operational permissions (view dashboard, manage sponsorship requests, manage sponsor settings)
### 7.4 Team
- `team_owner`: all team permissions for that team
- `team_admin`: team management permissions (update team, manage roster, handle join requests)
---
## 8) Membership Resolvers (Clean Architecture Boundary)
Authorization needs a clean boundary for “does actor have a scoped role for this resource?”
Conceptually:
- League membership repository answers: actors role in leagueId
- Team membership repository answers: actors role in teamId
- Sponsor membership repository answers: actors role in sponsorId
This keeps persistence details out of controllers and allows in-memory adapters for tests.
---
## 9) Example Endpoint Policies (Conceptual)
### 9.1 Public read
- Public league standings page:
- Feature availability: `league.public` view (if you want to gate)
- Authorization: public route (no login)
### 9.2 League admin mutation
- Remove a member from league:
- Requires login
- Requires league scope
- Requires `league.admin.members` mutate
- Returns 403 if not allowed; 404 only if non-disclosure is intended
### 9.3 Stewarding review
- Review protest:
- Requires login
- Requires league scope derived from the protests race/league
- Requires `league.stewarding.protests` mutate
- Actor must be derived from session, not from request body
### 9.4 Payments
- Payments endpoints:
- Requires login
- Likely requires system `admin` or `owner`
---
## 10) Data Flow (Conceptual)
```mermaid
flowchart LR
Req[HTTP Request] --> AuthN[Authenticate actor]
AuthN --> Scope[Resolve resource scope]
Scope --> Roles[Load actor roles for scope]
Roles --> Perms[Evaluate required permissions]
Perms --> Allow{Allow}
Allow -->|Yes| Handler[Route handler]
Allow -->|No| Deny[Deny 401 or 403 or 404]
```
Rules:
- AuthN attaches actor identity to the request.
- Scope resolution loads resource context (leagueId, teamId, sponsorId) from route params or from looked-up entities.
- Required permissions must be declared at the boundary (controller/route metadata).
- Deny-by-default means anything not marked public requires an actor.
---
## 11) What This Enables Later
- A super-admin UI can manage:
- global roles (owner/admin)
- scoped roles (league_owner/admin/steward, sponsor_owner/admin, team_owner/admin)
- Feature availability remains a separate control plane (maintenance mode, coming soon, kill switches), documented in [`docs/architecture/shared/FEATURE_AVAILABILITY.md`](docs/architecture/shared/FEATURE_AVAILABILITY.md:1).