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

102 lines
3.9 KiB
Markdown

# 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`