Files
gridpilot.gg/docs/architecture/api/AUTHORIZATION.md
2026-01-11 13:04:33 +01:00

7.7 KiB
Raw Blame History

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/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:

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

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/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 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)

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/FEATURE_AVAILABILITY.md.