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)
- Client metadata supports both:
tenant(scalar compatibility/default tenant)tenants(space-delimited assignment set; normalized lowercase, unique, sorted)
- Token request may include
tenant=<id>. - Authority resolves selected tenant deterministically:
- If
tenantparameter is present: it must exist in assigned tenants. - If no parameter:
- use scalar
tenantwhen configured, otherwise - use single-entry
tenants, otherwise - reject as ambiguous.
- use scalar
- If
- Issued tokens carry:
stellaops:tenant(selected tenant)stellaops:allowed_tenants(space-delimited assigned set, optional)
- 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_requestdepending 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
tenantduring token issuance.
References
docs/implplan/SPRINT_20260222_053_DOCS_multi_tenant_same_api_key_contract_baseline.mddocs/implplan/SPRINT_20260222_054_Authority_same_key_multi_tenant_token_selection.mddocs/implplan/SPRINT_20260222_055_Router_tenant_header_enforcement_and_selection_flow.md