# 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).