From f762161d84662cd80e6ddd05bbc59ba34f4720d8 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 19 Aug 2026 09:51:08 +0200 Subject: [PATCH] Adapt USER-WP-0021 and USER-WP-0023 to published policy-nexus contracts 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. --- docs/canon-interface-card.yaml | 5 ++- docs/flex-auth-caller-identity.md | 12 +++++- docs/interfaces/netkingdom-integration.md | 11 ++++++ docs/portal-threat-model.md | 5 ++- src/user_engine/adapters/claims.py | 1 + src/user_engine/adapters/flex_auth.py | 1 + src/user_engine/domain/models.py | 1 + src/user_engine/testing/fixtures.py | 1 + tests/test_platform_adapters.py | 3 +- tests/test_verified_claims.py | 6 +++ wiki/ArchitectureBlueprint.md | 11 +++++- .../USER-WP-0021-portal-product-expansion.md | 38 +++++++++++++++++-- .../USER-WP-0023-flex-auth-caller-identity.md | 24 +++++++++++- 13 files changed, 109 insertions(+), 10 deletions(-) diff --git a/docs/canon-interface-card.yaml b/docs/canon-interface-card.yaml index 26228fb..7d5a930 100644 --- a/docs/canon-interface-card.yaml +++ b/docs/canon-interface-card.yaml @@ -4,7 +4,7 @@ description: > tenant, membership, profile, lifecycle, and evidence-facing context. status: candidate owner: codex -updated: "2026-06-05" +updated: "2026-08-19" implements: - identity-canon conceptual model as an implementation-facing domain facade @@ -25,9 +25,10 @@ produces: - Access Grant or grant-like membership fact consumes: - - NetKingdom IAM Profile claims + - NetKingdom IAM Profile v0.3 claims - verified issuer and subject identifiers - assurance and principal type claims + - optional tenant_roles forwarded to flex-auth only - authorization decisions and obligations - policy, control, review, exception, and evidence references - lifecycle task references from downstream task systems diff --git a/docs/flex-auth-caller-identity.md b/docs/flex-auth-caller-identity.md index ccc80b2..e08d015 100644 --- a/docs/flex-auth-caller-identity.md +++ b/docs/flex-auth-caller-identity.md @@ -2,6 +2,14 @@ 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 ServiceAccount token whose audience is exactly `flex-auth`. The adapter reads 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 with `resource.system` set to `tenant-engine`. Expect a refusal from the 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 user-engine digest, in USER-WP-0023-T03 before marking it done. A pass on diff --git a/docs/interfaces/netkingdom-integration.md b/docs/interfaces/netkingdom-integration.md index 2da30ef..48cb571 100644 --- a/docs/interfaces/netkingdom-integration.md +++ b/docs/interfaces/netkingdom-integration.md @@ -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` (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 | Port | Direction | Purpose | @@ -55,6 +65,7 @@ actor: tenant: string principal_type: human | service | agent roles: [string] + tenant_roles: [string] groups: [string] scopes: [string] assurance: object diff --git a/docs/portal-threat-model.md b/docs/portal-threat-model.md index 6312ee3..534563f 100644 --- a/docs/portal-threat-model.md +++ b/docs/portal-threat-model.md @@ -46,4 +46,7 @@ as externally complete. Compensation is idempotent and operator-visible. Enterprise SAML/OIDC federation, SCIM and corporate directory synchronization remain provider adapters. Their external ownership metadata and identity links 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. diff --git a/src/user_engine/adapters/claims.py b/src/user_engine/adapters/claims.py index fe44256..bc8ff2a 100644 --- a/src/user_engine/adapters/claims.py +++ b/src/user_engine/adapters/claims.py @@ -32,6 +32,7 @@ class VerifiedIdentityClaimsAdapter: principal_type=PrincipalType(str(claims["principal_type"])), audience=audience, roles=_strings(claims.get("roles", ())), + tenant_roles=_strings(claims.get("tenant_roles", ())), groups=_strings(claims.get("groups", ())), scopes=_strings(scopes), assurance=dict(claims.get("assurance", {})), diff --git a/src/user_engine/adapters/flex_auth.py b/src/user_engine/adapters/flex_auth.py index ad33632..fef320b 100644 --- a/src/user_engine/adapters/flex_auth.py +++ b/src/user_engine/adapters/flex_auth.py @@ -39,6 +39,7 @@ class FlexAuthHTTPAdapter: "attributes": { "issuer": request.actor.issuer, "roles": list(request.actor.roles), + "tenant_roles": list(request.actor.tenant_roles), "groups": list(request.actor.groups), "scopes": list(request.actor.scopes), "assurance": dict(request.actor.assurance), diff --git a/src/user_engine/domain/models.py b/src/user_engine/domain/models.py index 2e69d4d..fe5ae60 100644 --- a/src/user_engine/domain/models.py +++ b/src/user_engine/domain/models.py @@ -210,6 +210,7 @@ class Actor: principal_type: PrincipalType audience: tuple[str, ...] roles: tuple[str, ...] = () + tenant_roles: tuple[str, ...] = () groups: tuple[str, ...] = () scopes: tuple[str, ...] = () assurance: Mapping[str, Any] = field(default_factory=dict) diff --git a/src/user_engine/testing/fixtures.py b/src/user_engine/testing/fixtures.py index 2450499..3c70f65 100644 --- a/src/user_engine/testing/fixtures.py +++ b/src/user_engine/testing/fixtures.py @@ -38,6 +38,7 @@ class FixtureIdentityClaimsAdapter: principal_type=PrincipalType(str(claims["principal_type"])), audience=tuple(_as_tuple(claims.get("aud", ()))), roles=tuple(_as_tuple(claims.get("roles", ()))), + tenant_roles=tuple(_as_tuple(claims.get("tenant_roles", ()))), groups=tuple(_as_tuple(claims.get("groups", ()))), scopes=tuple(_as_tuple(scopes)), assurance=dict(claims.get("assurance", {})), diff --git a/tests/test_platform_adapters.py b/tests/test_platform_adapters.py index d7bb6a5..acde039 100644 --- a/tests/test_platform_adapters.py +++ b/tests/test_platform_adapters.py @@ -37,6 +37,7 @@ class PlatformAdapterTests(unittest.TestCase): request = json.loads(call.call_args.args[0].data) self.assertEqual(request["resource"]["system"], "user-engine") self.assertEqual(request["context"]["self"], True) + self.assertEqual(request["subject"]["attributes"]["tenant_roles"], ["CUS"]) def test_flex_auth_fails_closed_when_unavailable(self): with patch("user_engine.adapters.flex_auth.urlopen", side_effect=URLError("down")): @@ -155,7 +156,7 @@ def _request(): actor = Actor( issuer="https://issuer", subject="subject-1", tenant="tenant-a", principal_type=PrincipalType.HUMAN, audience=("user-engine",), - roles=("tenant-admin",), + roles=("tenant-admin",), tenant_roles=("CUS",), ) return AuthorizationRequest( actor=actor, resource_type="user-engine:user", resource_id="user-1", diff --git a/tests/test_verified_claims.py b/tests/test_verified_claims.py index f27857c..4c2c2c6 100644 --- a/tests/test_verified_claims.py +++ b/tests/test_verified_claims.py @@ -23,6 +23,12 @@ class VerifiedIdentityClaimsAdapterTests(unittest.TestCase): actor = self.adapter.normalize(self.claims) self.assertEqual("person-1", actor.subject) 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): with self.assertRaises(ValidationError): diff --git a/wiki/ArchitectureBlueprint.md b/wiki/ArchitectureBlueprint.md index db35e71..0b6e7a2 100644 --- a/wiki/ArchitectureBlueprint.md +++ b/wiki/ArchitectureBlueprint.md @@ -5,7 +5,9 @@ Date: 2026-05-22 Related product docs: `../INTENT.md`, `ProductRequirementsDocument.md` 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/responsibility-map.md` - `/home/worsch/key-cape/SCOPE.md` @@ -63,10 +65,17 @@ by flex-auth: - `iss`, `sub`, `aud`, `tenant`, `principal_type` - `groups`, `roles`, `scope` or `scp` - `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`, and `name` - 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, username, or display name must never be primary keys. diff --git a/workplans/USER-WP-0021-portal-product-expansion.md b/workplans/USER-WP-0021-portal-product-expansion.md index 1c00370..f7fbbb6 100644 --- a/workplans/USER-WP-0021-portal-product-expansion.md +++ b/workplans/USER-WP-0021-portal-product-expansion.md @@ -4,11 +4,11 @@ type: workplan title: "Expand user-engine portal beyond the proven Binky MVP" domain: communication repo: user-engine -status: active +status: finished owner: codex topic_slug: netkingdom created: "2026-07-30" -updated: "2026-08-16" +updated: "2026-08-19" depends_on: - USER-WP-0020 - TEN-WP-0005 @@ -190,7 +190,7 @@ deployed and wants its own permission. ```task id: USER-WP-0021-T05 -status: wait +status: done priority: low 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 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) 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 that failed rows are not filtered out. The full suite passes 133 tests with 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. diff --git a/workplans/USER-WP-0023-flex-auth-caller-identity.md b/workplans/USER-WP-0023-flex-auth-caller-identity.md index ac86853..df183da 100644 --- a/workplans/USER-WP-0023-flex-auth-caller-identity.md +++ b/workplans/USER-WP-0023-flex-auth-caller-identity.md @@ -8,7 +8,7 @@ status: active owner: codex topic_slug: netkingdom created: "2026-08-18" -updated: "2026-08-18" +updated: "2026-08-19" 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 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`.