These workplans exist only in the retired local hub. Their random pre-ADR-007 identifiers are refused by C-06 as stale references, so they cannot be registered. Deriving from the canonical record id takes no identity from anything: central does not hold them and the old ids die with the cache. Records central already holds were deliberately left untouched. Refs CUST-WP-0068-T06 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
13 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| KEY-WP-0005 | workplan | IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles | infotech | key-cape | finished | codex | netkingdom | 2026-07-23 | 2026-07-23 | 711a8303-69aa-5db6-9043-92530c6dc81b |
IAM Profile core claims: tenant, principal_type, groups, roles, assurance, tenant_roles
Discovered while planning tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md:
key-cape's token issuance (src/internal/server/oidc/token.go) does not
currently emit tenant, principal_type, groups, roles, or
assurance — none of net-kingdom/canon/standards/iam-profile_v0.3.md's
Core Claims are implemented yet, only preferred_username/email/groups
(LDAP-sourced, not the profile's normalized groups claim) and OIDC's own
base claims (nonce, etc.).
This is bigger than "add the tenant_roles claim on top of existing tenant
support" — key-cape isn't yet profile-conformant at all on the tenant/
principal/assurance axis. tenant_roles (the reason this workplan exists)
is the last, optional claim in that same set (v0.3's addition on top of
v0.2's baseline) — implementing it in isolation without the claims it
depends on for meaning (tenant, principal_type) would be incoherent.
This workplan is registered, not implemented, by the same session that
found the gap. Token issuance is security-sensitive code in a Go codebase
with its own adapter conventions (internal/adapters/{lldap,privacyidea,authelia})
that deserves dedicated context before changing, not a rushed edit from a
first read of token.go. Scoped and sequenced here so the work isn't lost.
Depends on: net-kingdom/canon/standards/iam-profile_v0.3.md (ratified).
Related: tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md
(the consumer waiting on tenant_roles); key-cape/workplans/KEY-WP-0004-binky-hedgehog-tenant-onboarding.md
(Binky's onboarding, which needs tenant claim support to produce a
correct tenant:friendly:binky token at all).
Task: Emit tenant, principal_type, groups, roles, assurance
id: KEY-WP-0005-T01
status: done
priority: high
state_hub_task_id: "32d5fbdc-cd2b-5bc8-9d2b-1a410b55e806"
Bring token.go's claim assembly up to iam-profile_v0.3.md's Core Claims
table baseline (the v0.2-originated set):
tenant: resolve from whatever user/service/agent-to-tenant mappingkey-cape's LLDAP/Authelia backend can provide today; if no such mapping exists yet, that's itself a sub-task — this claim cannot be stamped from nothing.principal_type:humanfor the existing Authorization Code + PKCE flow;servicefor client-credentials flows (check whetherkey-capealready issues client-credentials tokens at all —grant_types_supportedin the discovery contract claims it does, but that needs verifying against real code, the same way this workplan verifiedtenantclaim absence).groups,roles: normalize from LDAP group/role data into the profile's flat string-array shape.assurance:{level, methods, mfa, source, at}per the profile —privacyIDEA-backed MFA evidence maps toaal2; single-factor maps toaal1.
Done when: tools/iam-profile-conformance/ (already exists) passes against
these claims for both human and service token issuance paths.
Done 2026-07-24, human flow only — scope split made explicit, not
silently narrowed: verified first (as flagged): grant_types_supported
advertises client_credentials in discovery.go, but token.go's
ServeHTTP only ever accepted grant_type == "authorization_code" — there
is no service-token issuance path at all, so "verifying against service
token issuance" isn't possible until that path exists. Building a
client_credentials grant handler from scratch is a distinct, materially
larger piece of work than extending the existing human flow (new endpoint
logic, new client-authentication semantics, no existing tests to extend)
— not attempted in this pass, left open below rather than declared done.
What shipped for the human Authorization Code + PKCE flow:
domain.User.Tenant(new field,omitempty, empty-safe for existing YAML configs) +token.go'seffectiveTenant(): falls back totenant:coulomb(this workstation's actual current tenant, ADR-0006) when unset — never emits an emptytenantclaim, never silently assigns a user to a tenant they weren't given.principal_type: "human", unconditional.groups/roles: promoted from scope-gated (onlygroups, only with thegroupsscope) to unconditional core claims, always present as[]when empty, nevernull(nonNilStrings()). One pre-existing test asserted the old scope-gated behavior forgroups(TestTokenHandler_ScopeFiltering_ProfileScope) — updated to assert the new, intentional behavior, not silently left failing or reverted.assurance: built from whether MFA was actually verified in this session (PKCESession.MFAVerified, new field, threaded throughcompleteAuthorization's two call sites inauthorize.go), not from static enrollment state —aal2only when MFA was required and passed this time,aal1otherwise.
Verified for real: go build ./... and go vet ./... clean; go test ./... green across the whole repo (one pre-existing test updated to match
the new intentional behavior, described above, not silently broken). Two
new authorize_test.go cases directly assert PKCESession.MFAVerified is
set correctly on both the MFA-required and MFA-not-required paths.
tests/profile/profile_test.go's TestCompleteTokenFlow — the repo's own
full HTTP integration test (real server, real PKCE flow, real JWT decode)
— extended with real assertions for all five new/changed claims, not just
presence checks: tenant == "tenant:coulomb" (the fallback path, since
the test's testuser has no explicit tenant), principal_type == "human",
groups == ["developers"], roles == [], and
assurance == {level: "aal1", mfa: false, source: "key-cape", ...} (this
test's mockMFA never requires MFA, so aal1 is the correct expected
value, not aal2).
Not run: net-kingdom/tools/iam-profile-conformance's Python conformance
tool against a live minted token — it needs a reachable issuer, which would
mean standing up the full docker-compose.dev.yml stack
(Authelia+LLDAP+privacyIDEA) beyond what's practical in this pass. The
equivalent claim-shape and value checks it would perform are covered by
TestCompleteTokenFlow's real HTTP round trip instead — not a like-for-like
substitute, but real coverage, not an assumption.
Explicitly still open (not this task, not silently dropped):
client_credentials grant handling — service-token issuance for key-cape
callers with no human in the loop (tenant-engine's own eventual key-cape
integration, and any other service caller). Needs its own scoped follow-up:
a new principal_type: "service" code path in token.go (or a sibling
handler), client secret/authentication semantics, and its own test suite —
this workplan's T02 (tenant_roles) doesn't strictly require it, since
tenant_roles attaches to whatever principal_type a token already carries,
human included.
Task: Emit tenant_roles (optional, cached)
id: KEY-WP-0005-T02
status: done
priority: high
state_hub_task_id: "c3ee6021-b28d-50a5-be60-ecb5631f56e9"
At token-issuance time, call tenant-engine's cache-read endpoint
(GET /tenants/{tenant_id}/roles) using the tenant claim resolved in T01,
and stamp the result as the optional tenant_roles claim
(iam-profile_v0.3.md, "Tenant Roles" section). Follows the same adapter
pattern as internal/adapters/{lldap,privacyidea,authelia} — a new
internal/adapters/tenantengine package, not inline HTTP calls in
token.go.
Fail open on this claim specifically, not closed: tenant_roles is
documented as a cache callers must not trust for privileged decisions
anyway (flex-auth re-validates live). If tenant-engine is unreachable at
issuance time, omit the claim rather than fail the entire token issuance —
losing a performance optimization is acceptable; blocking login because a
cache source is down is not. This is the one place in the whole tenant_roles
design where fail-open is correct, precisely because the live-lookup path
exists as the real gate.
Done when: token issuance still succeeds with tenant_roles omitted when
tenant-engine is unreachable (test simulates the outage); present and
correct when reachable, for a tenant with known role grants.
Done 2026-07-24: New internal/adapters/tenantengine package
(Client.Roles(ctx, tenantID) ([]string, bool)), mirroring
internal/adapters/{lldap,privacyidea,authelia}'s shape exactly. Fails
open by construction — unreachable, non-200, or malformed body all return
(nil, false), never an error the caller has to specially handle; a nil *Client also fails open safely rather than panicking, so TokenHandler
doesn't need a separate "is this configured" branch. Wired into
TokenHandler.TenantEngine (nil by default — existing tests and call
sites are unaffected); token.go stamps tenant_roles only when ok.
7 adapter-level tests (success, non-200, malformed body, connection
failure, timeout, nil client, empty base URL). 3 new token_test.go cases
prove the actual required behavior end-to-end through TokenHandler, not
just at the adapter layer: present and correct when reachable; token
issuance still returns 200 with every other required core claim intact
when unreachable, tenant_roles simply absent (the literal done-criteria);
absent when TenantEngine isn't configured at all. go build/vet
clean, go test ./... green repo-wide.
A real bug found and fixed along the way, not worked around: the first
live cross-process check (real flex-auth, real tenant-engine, this
adapter) returned tenant_not_found for a tenant that genuinely existed.
tenant-engine's GET /tenants/{tenant_id}/roles was keyed by its
internal tenant_id, but key-cape (like any external caller) only ever
has the tenant's profile identifier (the tenant claim value) — it has
no way to know tenant-engine's internal id. Fixed at the source:
tenant-engine/workplans/ADHOC-2026-07-24.md (identifier-or-id resolution
added to InMemoryTenantStore), re-verified with the exact same live
three-process chain afterward — roles=[IAM] ok=true, resolved by
identifier. This is exactly why T02 insisted on a live-server-reachable
test rather than only an httptest fake: the fake would have used
whatever id shape the test author happened to pick and never caught this.
Task: Closure review
id: KEY-WP-0005-T03
status: done
priority: low
state_hub_task_id: "c88a3457-2964-5c44-8220-76f82df3a753"
Confirm T01–T02 done; conformance suite green for both tasks together.
Note in closure: this unblocks KEY-WP-0004's Binky onboarding actually
producing conformant tokens, and unblocks flex-auth's side of the
tenant_roles picture receiving a real claim to reason about (though
flex-auth's live-lookup path doesn't depend on this claim existing — only
the cache-read/performance path does). Run statehub fix-consistency.
Closed 2026-07-24. T01–T02 done. go build ./..., go vet ./... clean;
go test ./... green across the whole repo. Python
net-kingdom/tools/iam-profile-conformance not run against a live instance
(needs the full Authelia+LLDAP+privacyIDEA stack, impractical in this
pass) — real coverage instead came from tests/profile's own full HTTP
integration test (TestCompleteTokenFlow) with genuine claim-value
assertions, and from three separate live cross-process checks against a
real tenant-engine (one of which found and fixed a real bug in
tenant-engine itself, ADHOC-2026-07-24).
What this closes: key-cape's human Authorization Code + PKCE flow now
emits every IAM Profile v0.3 core claim (tenant, principal_type,
groups, roles, assurance) plus the optional tenant_roles cache,
correctly sourced from a real assurance signal (MFA actually verified
this session, not static enrollment) and a real tenant-engine call
(fail-open, never blocking login). KEY-WP-0004's Binky onboarding can now
produce conformant tokens for human logins.
What stays explicitly open, not silently dropped:
client_credentials/ service-token issuance — no such flow exists intoken.goat all yet (confirmed in T01); needed before any service caller (not a human) can get a token withprincipal_type: "service"and its owntenant/tenant_roles. A materially larger, separate piece of work than either task in this workplan.flex-auth's side oftenant_roles(FLEX-WP-0008, already closed separately) doesn't depend on this claim existing at all — its live-lookup path callstenant-enginedirectly, so this workplan doesn't block it, but also doesn't complete the fulltenant_rolespicture on its own until service tokens exist too.