Files
git.stella-ops.org/docs/architecture/decisions/ADR-002-multi-tenant-same-api-key-selection.md

3.9 KiB

ADR-002: Multi-Tenant Selection With Same API Key

Status: Accepted
Date: 2026-02-22
Sprint: SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.md

Context

Stella Ops must support clients that are assigned to more than one tenant while still using the same API key/client registration. Existing behavior assumes a scalar tenant assignment (tenant) and cannot safely select among multiple tenant memberships.

Without a canonical contract, modules diverge on claim names, header behavior, default selection, and mismatch handling. This creates cross-tenant leakage risk and migration churn.

Decision

Use one selected tenant per access token, chosen at token issuance time.

Selected model (accepted)

  1. Client metadata supports both:
    • tenant (scalar compatibility/default tenant)
    • tenants (space-delimited assignment set; normalized lowercase, unique, sorted)
  2. Token request may include tenant=<id>.
  3. Authority resolves selected tenant deterministically:
    • If tenant parameter is present: it must exist in assigned tenants.
    • If no parameter:
      • use scalar tenant when configured, otherwise
      • use single-entry tenants, otherwise
      • reject as ambiguous.
  4. Issued tokens carry:
    • stellaops:tenant (selected tenant)
    • stellaops:allowed_tenants (space-delimited assigned set, optional)
  5. Gateway and services continue operating with one effective tenant per request.

Fallback model (rejected for default path)

Multi-tenant token + per-request header override (X-StellaOps-Tenant) as primary selector.

Reason rejected:

  • Increases header spoofing and token confusion risk.
  • Creates inconsistent downstream behavior where services interpret tenant from different sources.
  • Expands change surface across all modules immediately.

Canonical Contract

Claims

  • stellaops:tenant: selected tenant for this token.
  • stellaops:allowed_tenants: assigned tenant set (space-delimited, sorted).

Client metadata

  • tenant: scalar assignment / deterministic default.
  • tenants: assigned set (space-delimited).

Headers

  • Canonical tenant header: X-StellaOps-Tenant.
  • Legacy compatibility header: X-Stella-Tenant (bounded migration use only).

Error semantics

  • Requested tenant not assigned: reject invalid_request.
  • Missing tenant for tenant-required scope: reject invalid_client / invalid_request depending on grant validation stage.
  • Ambiguous tenant selection (multi-assigned, no default, no request): reject invalid_request.
  • Token tenant not in client assignments during validation: reject invalid_token.

Threat Model

Header spoofing

Risk: caller supplies tenant headers to escalate into another tenant.
Mitigation: gateway strips inbound identity headers and rewrites from validated claims.

Token confusion

Risk: token tenant differs from issuing client assignment or persisted token document.
Mitigation: validation enforces principal/document/client assignment consistency.

Cross-tenant leakage

Risk: silent default tenant fallback routes tenant-scoped requests incorrectly.
Mitigation: remove authenticated "default" tenant fallback and fail ambiguous selections.

Consequences

Positive

  • Keeps downstream service model stable: one tenant per token/request.
  • Enables same-key multi-tenant clients without global API contract break.
  • Supports UI hydration with explicit assigned tenant set.

Tradeoff

  • Tenant switching requires requesting a token for the target tenant.
  • Multi-assigned clients without explicit default must send tenant during token issuance.

References

  • docs/implplan/SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.md
  • docs/implplan/SPRINT_20260222_054_Authority_same_key_multi_tenant_token_selection.md
  • docs/implplan/SPRINT_20260222_055_Router_tenant_header_enforcement_and_selection_flow.md