7.9 KiB
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
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:
capabilityKeyactionType(viewormutate)
Examples:
league.admin.members+mutateleague.stewarding.protests+viewsponsors.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:
- System roles apply everywhere.
- Scoped roles apply only when the request targets that scope.
Examples:
- A user can be
league_adminin League A and justuserin League B. - A system
adminis 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:
performerDriverIdadminIdstewardIdmust 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.
7) Suggested Role → Permission Mapping (First Pass)
This table is a starting point (refine as product scope increases).
7.1 System
owner: all permissionsadmin: platform-admin permissions (payments admin, sponsor portal admin, moderation)
7.2 League
league_owner: all league permissions for that leagueleague_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 sponsorsponsor_admin: sponsor operational permissions (view dashboard, manage sponsorship requests, manage sponsor settings)
7.4 Team
team_owner: all team permissions for that teamteam_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.publicview (if you want to gate) - Authorization: public route (no login)
- Feature availability:
9.2 League admin mutation
- Remove a member from league:
- Requires login
- Requires league scope
- Requires
league.admin.membersmutate - 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.protestsmutate - Actor must be derived from session, not from request body
9.4 Payments
- Payments endpoints:
- Requires login
- Likely requires system
adminorowner
10) Data Flow (Conceptual)
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.