HIP-118: SuperAdmin & Tenant Isolation Model. Status Draft. Hanzo's own standard — read this before implementing against it.
This is the one and only authorization model for the Hanzo platform. It defines a single primitive — the Org (tenant) — and exactly two admin scopes over it, which MUST NEVER be conflated:
isAdmin, org-scoped) — a customer administering their own orgon Hanzo IAM. Self-service. Not platform-privileged. The 99% path.
owner == "admin", membership of the reserved admin org) — the Hanzo platform sudo scope. The only cross-tenant identity, and the only one admitted to admin.hanzo.ai and every raw admin surface.
There is one predicate, one source of truth: an identity is a SuperAdmin if and only if its IAM owner claim equals the reserved admin org slug (admin). The edge gate, the console, and every subsystem evaluate the same predicate against the same claim. Trusting a per-org isAdmin flag for platform-level gating is a privilege-escalation bug, not an alternative implementation.
Adding a SuperAdmin is provision, never promote: an existing SuperAdmin creates a new named user in the admin org. A tenant user is never moved or promoted into admin. Together these rules make tenant isolation a property of the owner boundary and make platform privilege an auditable, individually attributable, separately-provisioned account.
HIP-0026 defines the IAM server (the Org, the owner field, the isAdmin flag). HIP-0111 defines how a client obtains and reads the owner claim. This HIP defines what those two values authorize. Where they touch, this HIP is authoritative on the authorization decision.
Reference implementation: ~/work/hanzo/gateway/cmd/admin-guard/main.go The predicate: owner == adminOrg (adminOrg default "admin", env IAM_ADMIN_ORG)
The estate is multi-tenant IAM-as-a-service: one IAM (Auth0/WorkOS-class) serves every brand and every customer. Customers do not fork or run their own IAM; they get an org and administer it themselves. In that world the dangerous question is not "is this user an admin?" but "an admin of what?". Every serious multi-tenant breach is a confusion of those two words. This HIP exists to make the answer unambiguous and mechanical, and it is written against the specific privilege-escalation classes it prevents:
isAdmin flag means "admin of my own org." If any platform surface gates on isAdmin alone, then any customer who is admin of their own org — which every self-service customer is — is admitted to the platform's cross-tenant surfaces. This is the highest-severity escalation: tenant admin → platform admin, reachable by design rather than by exploit. The fix is a different claim, not a stronger check on the same one: platform scope reads owner, never isAdmin.
tenant user (set a bit, add a role, join a group), then whoever can perform that mutation — or replay it, or find it under-guarded on one of N subsystems — escalates themselves. Provision-not-promote removes the mutation entirely: there is no bit to flip; SuperAdmin is a distinct account in a distinct org, created only by an existing SuperAdmin.
identity (their day-to-day org-admin account) and, separately, a platform SuperAdmin account. If a login silently resolves to whichever org is convenient, a routine session can acquire platform scope by accident. The model forces the admin org to be explicitly selected at authentication time and refuses to mint a platform session for any other resolved org.
owner and then defaults to a "default"/"personal"/empty org has just merged every unscoped request into one tenant — a cross-tenant read/write. Isolation on the owner boundary MUST fail closed (HIP-0111 §5), never fall back.
All four vanish under one primitive, two non-conflatable scopes, one predicate, and provision-not-promote. That is this standard.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as in RFC 2119.
The Org (tenant) is the only isolation primitive. Every user, application, credential, and datum belongs to exactly one Org, named by the IAM owner field (HIP-0026 schema: organization, user, application are all keyed on owner + name). Customer hierarchies (a customer's own sub-customers, B2B2C) are modeled as Orgs owned by Orgs, not as a second privilege axis. There is no tenant concept above or beside the Org. "Scope every query to owner" (HIP-0111 §5) is the concrete meaning of tenant isolation, and it is the same sentence at every layer.
| Scope | Predicate | Means | Surface | Who | |-------|-----------|-------|---------|-----| | Org admin | isAdmin == true (per-user, in that user's own org) | "admin OF YOUR OWN org" — manage your org's users, apps, and sub-orgs | console.hanzo.ai (self-service) | Every self-service customer. The 99% path. | | SuperAdmin | owner == "admin" (membership of the reserved admin org) | Hanzo platform sudo — the only cross-tenant scope | admin.hanzo.ai + every raw admin surface | A small, named, separately-provisioned set. |
The two are orthogonal, and conflating them is a defect:
A user can be an org admin of acme and hold no platform scope; a SuperAdmin is a member of admin and needs no isAdmin bit anywhere to exercise platform scope.
isAdmin MUST be used only for decisions within a single org (can this useredit their org's users/apps). It MUST NOT be read by any platform-level or cross-tenant gate.
owner == "admin" MUST be the only basis for admitting an identity to across-tenant or raw-admin surface.
An identity is a SuperAdmin if and only if:
owner == AdminOrg // AdminOrg is the reserved org slug, default "admin"
owner is the IAM org claim carried, per HIP-0111 §5, identically in the JWT and the OIDC userinfo response, in every token format, scope-independent. The predicate is evaluated from that claim and nothing else. Concretely:
decide().X-Org-Id (HIP-0044); downstream subsystems read X-Org-Id and apply the same predicate for any platform action, and scope every tenant query to it.
There MUST be exactly one predicate, spelled the same way everywhere. A subsystem that invents its own platform-admin test (a bespoke role name, a hardcoded email allowlist, a groups claim, a per-org isAdmin) is non-conformant even if it "works," because it drifts from the single source of truth and becomes an independent escalation surface.
Standard nomenclature is SuperAdmin. The terms "GlobalAdmin" / "global admin" MUST NOT be used in new code, config, or docs; existing occurrences (including IAM IsGlobalAdmin and the admin-guard docstrings, §7) are the legacy spelling of this exact predicate and are to be renamed to SuperAdmin — a nomenclature migration, not a logic change.
admin orgadmin is a reserved org slug. It is not a customer tenant and MUST NOT be issued to one. Its sole purpose is to be the set whose membership is platform privilege. Its name is configurable at the edge (IAM_ADMIN_ORG) for white-label parity, but the deployed value is fixed per network and known to every gate; the default is admin. Because SuperAdmin ⟺ membership of this org, the org's member list is the platform's complete, single-surface roster of privileged accounts — the object of periodic access review (§Security, AC-2(3)).
Adding platform privilege is a creation, never a mutation:
admin org: iam user create --owner admin. The account is individually named and attributable. A brand-org user is never** moved, copied, or promoted into admin; doing so duplicates an identity across orgs and destroys separation of duties.
isAdmin on a member of their org.This never crosses the org boundary and never touches platform scope.
A human who needs both scopes holds two accounts by design: their brand-org identity (e.g. the seeded z@hanzo.ai, an org admin) and a separate SuperAdmin account in admin. This dual identity is a feature — routine work carries only tenant scope; platform scope is a deliberate, separate login.
admin.hanzo.ai vs console.hanzo.aiTwo surfaces, one predicate deciding between them:
admin.hanzo.ai and every raw admin surface — the platform-operator surfaces: platform.hanzo.ai, studio, commerce-admin, the raw KMS admin UI, the IAM management UI. These are admitted to SuperAdmin only, gated by the admin-guard ForwardAuth middleware (§7) in front of hanzoai/ingress (HIP-0068). No raw admin surface is ever exposed without the gate.
console.hanzo.ai — the unified client surface for everyone: org admins andordinary members alike, each scoped to their own org. This is where self-service lives.
The routing rule is total and dead-end-free: an authenticated identity that is not a SuperAdmin is sent to console.hanzo.ai, never shown a 403 wall on a raw admin surface (browser clients). Only a non-browser API caller receives a status code (403/401) instead of a redirect, so automation gets a clean signal. An anonymous browser is sent to interactive IAM PKCE login (HIP-0111), not to a dead end.
Entry to the SuperAdmin scope is standard OAuth2 Authorization-Code + PKCE S256 against IAM (HIP-0111), with the reserved admin org pinned on the authorization request (organization=admin). Pinning is mandatory and load-bearing: a human who is a member of both a tenant org and admin MUST resolve to their admin identity for the platform login, otherwise the login defaults to their home org and is correctly denied. The callback MUST refuse to mint a platform session for any resolved owner != admin (it redirects such an authenticated identity to console.hanzo.ai); it mints the SuperAdmin session only when the validated token's owner equals the admin org.
Identity MAY be resolved from more than one transport (a signed edge session cookie, a Bearer/Basic JWT, an IAM SSO session), but all transports MUST collapse to the single §3 predicate. No transport may widen the scope.
This model is the control that implements the following NIST SP 800-53 Rev. 5 requirements; the mapping is the compliance contract (SOC 2 / FedRAMP):
| Control | Requirement | How this HIP satisfies it | |---------|-------------|---------------------------| | AC-6(5) | Privileged accounts restricted to designated personnel | SuperAdmin is membership of the reserved admin org (§4), a distinct account set — not a flag on a tenant user. The privileged population is exactly the admin-org roster. | | AC-5 | Separation of duties | Provision-not-promote (§5): platform identity and tenant identity are separate accounts. No single account holds both scopes; the two admin scopes are orthogonal and never conflated (§2). | | AC-2 / IA-2 | Account management; unique identification & authentication (incl. MFA) | SuperAdmins are individually named users created in admin (§5), each attributable; authentication is OIDC + PKCE with WebAuthn/MFA available per HIP-0026. No shared or anonymous platform account. | | AC-6(2) / AC-6 | Least privilege; non-privileged access for non-security functions; JIT / break-glass | Dual identity (§5) means routine work uses the org-admin account (tenant scope only); the SuperAdmin account is used only to operate the platform — least privilege by default, break-glass by deliberate separate login. | | AU-2 / AU-12 | Auditable events; audit record generation | The edge gate emits the resolved org downstream (X-Org-Id) for app-side auditing of every privileged action (§7, reference impl); because privilege is a single predicate over a single claim, every privileged decision is uniform and loggable. | | AC-2(3) | Disable / review accounts | The admin org membership (§4) is the single review surface — one list enumerates every platform-privileged identity for periodic access review; deprovisioning is removal from that org. | | SC-2 / AC-4 | Application partitioning; information-flow enforcement | Tenant isolation is the owner boundary (§1): every query is scoped to owner, the gateway propagates it as X-Org-Id, and cross-tenant flow exists only under the single SuperAdmin scope. Reading owner MUST fail closed, never fall back to a default org (HIP-0111 §5). |
Additional considerations:
owner MUST reject the request. Falling back to a "default", "personal", or empty org silently merges tenants and is a reportable isolation defect (SC-2/AC-4).
status code; browsers are redirected to the correct surface. Neither path ever widens scope; the asymmetry is only in how denial is delivered.
membership, not on the obscurity of the slug. IAM_ADMIN_ORG is configuration for white-label parity, not a credential.
mint their own admin-org membership because they do not operate the issuer.
~/work/hanzo/gateway/cmd/admin-guard/main.go is the canonical enforcement point: a single ForwardAuth gate consumed by hanzoai/ingress (HIP-0068) at GET /__guard/verify, in front of every raw admin surface. It is the concrete, production form of §3, §6, and §7.
decide() admits the caller only when owner != "" && owner == c.adminOrg; on match it sets X-Org-Id and X-Admin-Guard: allow and returns 204. adminOrg is loaded from IAM_ADMIN_ORG, default "admin". This is the one source of truth; the docstring states it verbatim: "ONE predicate, one source of truth."
to console.hanzo.ai (302); a non-browser caller gets 403 "global admin required"; an anonymous browser is sent to IAM PKCE login. No raw admin 403 wall for browsers.
startLogin() sets organization=adminOrg on the authorize request so a dual-member resolves to their admin identity; handleCallback() refuses to set a session when owner != adminOrg and redirects to the console instead.
orthogonal sources — the guard's signed session cookie, a Bearer/Basic JWT validated through iamauth (the JWT already carries owner, so no IAM round-trip), and an IAM session cookie resolved via get-account — all funneling into the same owner == adminOrg test. No source widens scope.
Nomenclature migration (§3). The file currently uses the legacy spelling — the docstring says "GLOBAL ADMINS ONLY" and refers to IAM IsGlobalAdmin. The logic is already this standard exactly (owner == c.adminOrg); the identifiers and prose are to be renamed to SuperAdmin (IAM IsGlobalAdmin → IsSuperAdmin, console useIsGlobalAdmin → useIsSuperAdmin). This is a rename, and it changes no decision.
owner field, the isAdmin flag.owner claim; the fail-closed tenant rule (§5).X-Org-Id propagation to subsystems.~/work/hanzo/gateway/cmd/admin-guard/main.go — the reference implementation of the predicate and the two surfaces.Copyright and related rights waived via CC0.