257 lines
7.9 KiB
Markdown
257 lines
7.9 KiB
Markdown
# 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: actor’s role in leagueId
|
||
- Team membership repository answers: actor’s role in teamId
|
||
- Sponsor membership repository answers: actor’s 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 protest’s 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).
|