Adapt USER-WP-0021 and USER-WP-0023 to published policy-nexus contracts
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m1s

IAM Profile v0.3 and Tenancy Posture v0.1 are now live on
policy.coulomb.social. Close the portal expansion workplan against those
documents, keep the flex-auth live A2 probe waiting, and forward optional
tenant_roles to flex-auth without authorizing from them locally.
This commit is contained in:
tegwick 2026-08-19 09:51:08 +02:00
parent 166788228d
commit f762161d84
13 changed files with 109 additions and 10 deletions

View file

@ -4,7 +4,7 @@ description: >
tenant, membership, profile, lifecycle, and evidence-facing context. tenant, membership, profile, lifecycle, and evidence-facing context.
status: candidate status: candidate
owner: codex owner: codex
updated: "2026-06-05" updated: "2026-08-19"
implements: implements:
- identity-canon conceptual model as an implementation-facing domain facade - identity-canon conceptual model as an implementation-facing domain facade
@ -25,9 +25,10 @@ produces:
- Access Grant or grant-like membership fact - Access Grant or grant-like membership fact
consumes: consumes:
- NetKingdom IAM Profile claims - NetKingdom IAM Profile v0.3 claims
- verified issuer and subject identifiers - verified issuer and subject identifiers
- assurance and principal type claims - assurance and principal type claims
- optional tenant_roles forwarded to flex-auth only
- authorization decisions and obligations - authorization decisions and obligations
- policy, control, review, exception, and evidence references - policy, control, review, exception, and evidence references
- lifecycle task references from downstream task systems - lifecycle task references from downstream task systems

View file

@ -2,6 +2,14 @@
Status: caller side implemented and deployed; live proof pending flex-auth A2 promotion (FLEX-WP-0015-T02). Status: caller side implemented and deployed; live proof pending flex-auth A2 promotion (FLEX-WP-0015-T02).
Governing published policy (2026-08-19):
- IAM Profile v0.3 Service Account Flow — audience-scoped, rotating
workload identity:
`https://policy.coulomb.social/standards/iam-profile/v0.3/`
- Tenancy Posture v0.1 A2 — inbound caller choke point:
`https://policy.coulomb.social/standards/tenancy-posture/v0.1/`
user-engine calls `flex-auth-user-engine` with a projected Kubernetes user-engine calls `flex-auth-user-engine` with a projected Kubernetes
ServiceAccount token whose audience is exactly `flex-auth`. The adapter reads ServiceAccount token whose audience is exactly `flex-auth`. The adapter reads
the token file for every decision so hourly projection rotation requires no the token file for every decision so hourly projection rotation requires no
@ -77,7 +85,9 @@ BODY='{"actor":{"issuer":"https://kc.coulomb.social","subject":"probe",
3. **user-engine cannot represent another protected system.** Re-run step 1 3. **user-engine cannot represent another protected system.** Re-run step 1
with `resource.system` set to `tenant-engine`. Expect a refusal from the with `resource.system` set to `tenant-engine`. Expect a refusal from the
binding, not an allow: the token binds `user-engine` to binding, not an allow: the token binds `user-engine` to
`system:serviceaccount:user-engine:user-engine` only. `system:serviceaccount:user-engine:user-engine` only. That binding is the
A2 choke point named by Tenancy Posture v0.1; publication does not change
the remaining operator sequence.
Record all three results, the flex-auth digest they ran against, and the Record all three results, the flex-auth digest they ran against, and the
user-engine digest, in USER-WP-0023-T03 before marking it done. A pass on user-engine digest, in USER-WP-0023-T03 before marking it done. A pass on

View file

@ -13,6 +13,16 @@ See also the cross-repo assessment in the net-kingdom repo:
`/home/worsch/net-kingdom/docs/user-engine-netkingdom-integration-assessment.md` `/home/worsch/net-kingdom/docs/user-engine-netkingdom-integration-assessment.md`
(for current intent/scope fit, gaps, and recommendations as of 2026-06-03). (for current intent/scope fit, gaps, and recommendations as of 2026-06-03).
Published governing contracts (policy-nexus, 2026-08-19):
- IAM Profile v0.3: `https://policy.coulomb.social/standards/iam-profile/v0.3/`
- Tenancy Posture v0.1: `https://policy.coulomb.social/standards/tenancy-posture/v0.1/`
- NetKingdom architecture: `https://policy.coulomb.social/architecture/net-kingdom/v0.1/`
The user-engine boundary contract remains accepted in net-kingdom
(`canon/standards/user-engine-boundary-contract_v0.1.md`) and is marked
`publish` in the policy-nexus ledger, but it is not yet on the site.
## Required Ports ## Required Ports
| Port | Direction | Purpose | | Port | Direction | Purpose |
@ -55,6 +65,7 @@ actor:
tenant: string tenant: string
principal_type: human | service | agent principal_type: human | service | agent
roles: [string] roles: [string]
tenant_roles: [string]
groups: [string] groups: [string]
scopes: [string] scopes: [string]
assurance: object assurance: object

View file

@ -46,4 +46,7 @@ as externally complete. Compensation is idempotent and operator-visible.
Enterprise SAML/OIDC federation, SCIM and corporate directory synchronization Enterprise SAML/OIDC federation, SCIM and corporate directory synchronization
remain provider adapters. Their external ownership metadata and identity links remain provider adapters. Their external ownership metadata and identity links
must fit the existing neutral boundaries rather than becoming user-engine must fit the existing neutral boundaries rather than becoming user-engine
domain dependencies. domain dependencies. The published target is IAM Profile v0.3 Keycloak
expanded mode
(`https://policy.coulomb.social/standards/iam-profile/v0.3/`); a dedicated
workplan is created only when a tenant demands those integrations.

View file

@ -32,6 +32,7 @@ class VerifiedIdentityClaimsAdapter:
principal_type=PrincipalType(str(claims["principal_type"])), principal_type=PrincipalType(str(claims["principal_type"])),
audience=audience, audience=audience,
roles=_strings(claims.get("roles", ())), roles=_strings(claims.get("roles", ())),
tenant_roles=_strings(claims.get("tenant_roles", ())),
groups=_strings(claims.get("groups", ())), groups=_strings(claims.get("groups", ())),
scopes=_strings(scopes), scopes=_strings(scopes),
assurance=dict(claims.get("assurance", {})), assurance=dict(claims.get("assurance", {})),

View file

@ -39,6 +39,7 @@ class FlexAuthHTTPAdapter:
"attributes": { "attributes": {
"issuer": request.actor.issuer, "issuer": request.actor.issuer,
"roles": list(request.actor.roles), "roles": list(request.actor.roles),
"tenant_roles": list(request.actor.tenant_roles),
"groups": list(request.actor.groups), "groups": list(request.actor.groups),
"scopes": list(request.actor.scopes), "scopes": list(request.actor.scopes),
"assurance": dict(request.actor.assurance), "assurance": dict(request.actor.assurance),

View file

@ -210,6 +210,7 @@ class Actor:
principal_type: PrincipalType principal_type: PrincipalType
audience: tuple[str, ...] audience: tuple[str, ...]
roles: tuple[str, ...] = () roles: tuple[str, ...] = ()
tenant_roles: tuple[str, ...] = ()
groups: tuple[str, ...] = () groups: tuple[str, ...] = ()
scopes: tuple[str, ...] = () scopes: tuple[str, ...] = ()
assurance: Mapping[str, Any] = field(default_factory=dict) assurance: Mapping[str, Any] = field(default_factory=dict)

View file

@ -38,6 +38,7 @@ class FixtureIdentityClaimsAdapter:
principal_type=PrincipalType(str(claims["principal_type"])), principal_type=PrincipalType(str(claims["principal_type"])),
audience=tuple(_as_tuple(claims.get("aud", ()))), audience=tuple(_as_tuple(claims.get("aud", ()))),
roles=tuple(_as_tuple(claims.get("roles", ()))), roles=tuple(_as_tuple(claims.get("roles", ()))),
tenant_roles=tuple(_as_tuple(claims.get("tenant_roles", ()))),
groups=tuple(_as_tuple(claims.get("groups", ()))), groups=tuple(_as_tuple(claims.get("groups", ()))),
scopes=tuple(_as_tuple(scopes)), scopes=tuple(_as_tuple(scopes)),
assurance=dict(claims.get("assurance", {})), assurance=dict(claims.get("assurance", {})),

View file

@ -37,6 +37,7 @@ class PlatformAdapterTests(unittest.TestCase):
request = json.loads(call.call_args.args[0].data) request = json.loads(call.call_args.args[0].data)
self.assertEqual(request["resource"]["system"], "user-engine") self.assertEqual(request["resource"]["system"], "user-engine")
self.assertEqual(request["context"]["self"], True) self.assertEqual(request["context"]["self"], True)
self.assertEqual(request["subject"]["attributes"]["tenant_roles"], ["CUS"])
def test_flex_auth_fails_closed_when_unavailable(self): def test_flex_auth_fails_closed_when_unavailable(self):
with patch("user_engine.adapters.flex_auth.urlopen", side_effect=URLError("down")): with patch("user_engine.adapters.flex_auth.urlopen", side_effect=URLError("down")):
@ -155,7 +156,7 @@ def _request():
actor = Actor( actor = Actor(
issuer="https://issuer", subject="subject-1", tenant="tenant-a", issuer="https://issuer", subject="subject-1", tenant="tenant-a",
principal_type=PrincipalType.HUMAN, audience=("user-engine",), principal_type=PrincipalType.HUMAN, audience=("user-engine",),
roles=("tenant-admin",), roles=("tenant-admin",), tenant_roles=("CUS",),
) )
return AuthorizationRequest( return AuthorizationRequest(
actor=actor, resource_type="user-engine:user", resource_id="user-1", actor=actor, resource_type="user-engine:user", resource_id="user-1",

View file

@ -23,6 +23,12 @@ class VerifiedIdentityClaimsAdapterTests(unittest.TestCase):
actor = self.adapter.normalize(self.claims) actor = self.adapter.normalize(self.claims)
self.assertEqual("person-1", actor.subject) self.assertEqual("person-1", actor.subject)
self.assertEqual(("tenant-admin",), actor.roles) self.assertEqual(("tenant-admin",), actor.roles)
self.assertEqual((), actor.tenant_roles)
def test_optional_tenant_roles_are_preserved_without_local_authorization(self):
actor = self.adapter.normalize({**self.claims, "tenant_roles": ["CUS", "VEN"]})
self.assertEqual(("CUS", "VEN"), actor.tenant_roles)
self.assertEqual(("tenant-admin",), actor.roles)
def test_rejects_wrong_issuer_and_audience(self): def test_rejects_wrong_issuer_and_audience(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):

View file

@ -5,7 +5,9 @@ Date: 2026-05-22
Related product docs: `../INTENT.md`, `ProductRequirementsDocument.md` Related product docs: `../INTENT.md`, `ProductRequirementsDocument.md`
Primary platform references: Primary platform references:
- `/home/worsch/net-kingdom/canon/standards/iam-profile_v0.2.md` - `https://policy.coulomb.social/standards/iam-profile/v0.3/` (source: `/home/worsch/net-kingdom/canon/standards/iam-profile_v0.3.md`; v0.2 is superseded)
- `https://policy.coulomb.social/standards/tenancy-posture/v0.1/`
- `https://policy.coulomb.social/architecture/net-kingdom/v0.1/`
- `/home/worsch/net-kingdom/docs/platform-identity-security-architecture.md` - `/home/worsch/net-kingdom/docs/platform-identity-security-architecture.md`
- `/home/worsch/net-kingdom/docs/responsibility-map.md` - `/home/worsch/net-kingdom/docs/responsibility-map.md`
- `/home/worsch/key-cape/SCOPE.md` - `/home/worsch/key-cape/SCOPE.md`
@ -63,10 +65,17 @@ by flex-auth:
- `iss`, `sub`, `aud`, `tenant`, `principal_type` - `iss`, `sub`, `aud`, `tenant`, `principal_type`
- `groups`, `roles`, `scope` or `scp` - `groups`, `roles`, `scope` or `scp`
- `assurance` - `assurance`
- optional `tenant_roles` (IAM Profile v0.3 cached tenant capability roles;
forwarded to flex-auth, never used as a local authorization decision;
privileged or `aal2`-class actions must re-query `tenant-engine`)
- optional human display/contact claims such as `preferred_username`, `email`, - optional human display/contact claims such as `preferred_username`, `email`,
and `name` and `name`
- optional agent/delegation claims for agentic use cases - optional agent/delegation claims for agentic use cases
A tenant's onboarding grouping (ADR-0013) and its capability roles (ADR-0014)
are independent. The identifier's grouping segment is historical after a
reclassification; `tenant_roles` is not encoded in the identifier.
`user-engine` must treat `iss + sub` as the stable identity link key. Email, `user-engine` must treat `iss + sub` as the stable identity link key. Email,
username, or display name must never be primary keys. username, or display name must never be primary keys.

View file

@ -4,11 +4,11 @@ type: workplan
title: "Expand user-engine portal beyond the proven Binky MVP" title: "Expand user-engine portal beyond the proven Binky MVP"
domain: communication domain: communication
repo: user-engine repo: user-engine
status: active status: finished
owner: codex owner: codex
topic_slug: netkingdom topic_slug: netkingdom
created: "2026-07-30" created: "2026-07-30"
updated: "2026-08-16" updated: "2026-08-19"
depends_on: depends_on:
- USER-WP-0020 - USER-WP-0020
- TEN-WP-0005 - TEN-WP-0005
@ -190,7 +190,7 @@ deployed and wants its own permission.
```task ```task
id: USER-WP-0021-T05 id: USER-WP-0021-T05
status: wait status: done
priority: low priority: low
state_hub_task_id: "05046780-9625-47c2-8caf-f57e9239c603" state_hub_task_id: "05046780-9625-47c2-8caf-f57e9239c603"
``` ```
@ -199,6 +199,24 @@ When a tenant requires SAML/OIDC federation, SCIM, directory synchronization,
just-in-time provisioning, or customer-owned lifecycle, create a dedicated just-in-time provisioning, or customer-owned lifecycle, create a dedicated
cross-repository plan against the provider-neutral contracts. cross-repository plan against the provider-neutral contracts.
2026-08-19 policy-nexus adaptation: no tenant has demanded those
integrations, so this workplan does not spawn one. The trigger now points
at published contracts rather than unpublished drafts:
- IAM Profile v0.3 at
`https://policy.coulomb.social/standards/iam-profile/v0.3/` — Keycloak
expanded mode remains the interchangeable enterprise-federation
implementation; grouping and `tenant_roles` are independent axes
(ADR-0013 / ADR-0014). Existing v0.2 claim handling stays conformant.
- Tenancy Posture v0.1 at
`https://policy.coulomb.social/standards/tenancy-posture/v0.1/`.
- NetKingdom architecture draft-2 at
`https://policy.coulomb.social/architecture/net-kingdom/v0.1/`.
user-engine still does not own federation, SCIM, or directory
synchronization. A new workplan is the right vehicle if a tenant later
requires them. This workplan's product-expansion scope is complete.
## Production rollout (2026-08-08) ## Production rollout (2026-08-08)
Commit `8229c6d` passed 115 tests with three external-provider skips, was Commit `8229c6d` passed 115 tests with three external-provider skips, was
@ -283,3 +301,17 @@ instead of receiving the documented bounded automatic retry. The query now
selects every undelivered, non-dead-letter event; regression coverage checks selects every undelivered, non-dead-letter event; regression coverage checks
that failed rows are not filtered out. The full suite passes 133 tests with that failed rows are not filtered out. The full suite passes 133 tests with
three external-provider skips. three external-provider skips.
## Residuals after close (2026-08-19)
- Live tenant-lifecycle probe from a user-engine pod (GET / PATCH / retire /
reactivate on a disposable tenant) remains owed with USER-WP-0023-T03.
Contract tests cannot see a digest pin rollback.
- Outbox transport activation still waits on governed OpenBao event/mail
tokens and transactional SMTP credentials.
- Optional `tenant_roles` is now forwarded to flex-auth. Privileged
live re-query against tenant-engine is required only if a future policy
treats those roles as `aal2`-class input; flex-auth currently has no
such policy.
- A new workplan is required if a tenant demands federation, SCIM, or
directory synchronization.

View file

@ -8,7 +8,7 @@ status: active
owner: codex owner: codex
topic_slug: netkingdom topic_slug: netkingdom
created: "2026-08-18" created: "2026-08-18"
updated: "2026-08-18" updated: "2026-08-19"
state_hub_workstream_id: "014d0886-b690-4860-8337-c718e440f678" state_hub_workstream_id: "014d0886-b690-4860-8337-c718e440f678"
--- ---
@ -80,4 +80,26 @@ This task stays `wait` on FLEX-WP-0015-T02 promotion through FLEX-WP-0011,
and on an operator shell with cluster credentials, which agent sessions in and on an operator shell with cluster credentials, which agent sessions in
this repo do not hold. this repo do not hold.
2026-08-19 policy-nexus adaptation: the published contracts now name the
same split this workplan already implemented.
- IAM Profile v0.3 Service Account Flow
(`https://policy.coulomb.social/standards/iam-profile/v0.3/`) requires
audience-scoped, rotating workload credentials delivered through the
credential-management standard — that is the projected ServiceAccount
token we already send. The optional `tenant_roles` claim is forwarded
to flex-auth as a subject attribute and is never used as a local
authorization decision.
- Tenancy Posture v0.1
(`https://policy.coulomb.social/standards/tenancy-posture/v0.1/`) is the
public A2 target T03 is waiting to prove: a single inbound choke point
that refuses an unbound caller. Our half of that vector is deployed;
flex-auth's running digest is still A0.
- NetKingdom architecture draft-2
(`https://policy.coulomb.social/architecture/net-kingdom/v0.1/`) lists
both documents in chapter 9.
The workplan stays `active` because T03's live proof is still owed. That
proof is not unblocked by publication.
Contract: `docs/flex-auth-caller-identity.md`. Contract: `docs/flex-auth-caller-identity.md`.