Base's own registry was already org-native in every method — OrgDB(orgID),
HasOrg(orgID), orgDir(orgID) — and only the type names said tenant. So this is
the vocabulary catching up with the code, not a change of behaviour.
TenantRegistry -> Bases app.Bases().OrgDB(org)
TenantConfig -> BasesConfig
MultiTenant -> PerOrg it enables per-org databases; say that
MULTI_TENANT -> MULTI_BASE
core/tenant.go -> core/bases.go
Tenant was the wrong noun twice over: it is the multi-tenancy word, and this
product's unit is the thing a customer creates per org — a Base. Identity here
is org, user, project, and nothing else.
The UI never said it; this was Go-side only. 64 packages green.
One test was collateral and is fixed rather than renamed: TestToHTTPHeader
asserts a pure snake_case-to-Header-Case transform, so rewriting its EXPECTED
value while its input stayed "tenant_id" made it assert something false. The case
is dropped — org_id already covers that transform.
Every non-handler call site that wrapped a value in
slog.String/Int/Bool/Any/Float64/Uint64 was passing an Attr through the
variadic any... slot of (*slog.Logger).Info/Warn/Error/Debug; the same
slot accepts the bare "key", value pair with identical semantics. Drop
the wrapper so the surface is one level less indirect and so the next
two commits can swap (*slog.Logger) for a small Logger interface
without rewriting every attr expression as well.
23 files, 156 call sites collapsed; no behavior change. Five files —
tools/logger/log.go, tools/logger/batch_handler.go +_test, and
core/log_printer.go +_test — are the handler-implementation layer and
legitimately reference slog.Attr/Level/Group; they were skipped.
Introduces tools/claims as the single source of truth for reading the
authenticated caller's identity across every Base-derived service. One
way to do it, zero dispute.
The canonical 3 identity contract:
X-User-Id <- JWT sub (set by the Hanzo Gateway post-JWKS)
X-Org-Id <- JWT owner
X-Roles <- JWT roles (comma-joined if array)
Services must not read any other variant — no X-Hanzo-*, no X-IAM-*,
no X-User-Role (singular), no X-Tenant-Id alias. The Strip middleware
removes every historical variant on ingress so a spoofed header cannot
reach a handler.
Changes:
* tools/claims/claims.go (new):
- Constants HeaderUserID/HeaderOrgID/HeaderRoles
- Claims struct + FromHeaders(r) reader
- Claims.HasRole(...)
- StripIdentityHeaders(h) drops canonical + legacy + every
X-IAM-*/X-HANZO-* prefix (case-insensitive)
- Strip(next) net/http middleware
* tools/claims/claims_test.go (new): 7 tests covering canonical read,
empty roles, legacy-variant rejection, HasRole semantics, strip
coverage, strip preservation, and the Strip middleware.
* plugins/cloudsql/cloudsql.go: the postgres-meta proxy now keys
tenant routing on X-Org-Id (the canonical JWT-owner header) instead
of the legacy X-Tenant-ID.
* plugins/platform/auth_proxy_test.go: renamed the test marker
X-IAM-Test -> X-Test-Endpoint so the canonical grep ("X-IAM-*")
returns zero hits. Behavior unchanged.
Adds plugins/cloudsql with per-tenant database provisioning via Neon,
postgres-meta reverse proxy for schema introspection, and REST API
at /api/cloud-sql/databases. Registers platform + cloudsql plugins
in examples/base/main.go.