user-engine/workplans/USER-WP-0021-portal-product-expansion.md
tegwick f762161d84
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
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.
2026-08-19 09:51:08 +02:00

317 lines
16 KiB
Markdown

---
id: USER-WP-0021
type: workplan
title: "Expand user-engine portal beyond the proven Binky MVP"
domain: communication
repo: user-engine
status: finished
owner: codex
topic_slug: netkingdom
created: "2026-07-30"
updated: "2026-08-19"
depends_on:
- USER-WP-0020
- TEN-WP-0005
state_hub_workstream_id: "ba217f48-5fa5-4178-9c79-73aa225f3f2e"
---
# USER-WP-0021 - Portal product expansion
Preserve deliberately deferred product breadth from `USER-WP-0020` without
holding the proven production MVP open. Activate according to tenant demand.
## T01 - Complete invitation and platform recovery APIs
```task
id: USER-WP-0021-T01
status: done
priority: high
state_hub_task_id: "342299b8-d9a3-408d-bf0d-914496714d5f"
```
Add invitation claim/resend/expiry, platform tenant management and recovery
routes, optimistic concurrency, complete OpenAPI schemas, and durable outbox
delivery/replay/dead-letter operations.
2026-08-08 increment: durable invitations now carry expiry and an optimistic
version. The API supports tenant create/list, version-gated resend/expire, and
authenticated claim with terminal-state and stale-version rejection. Outbox
state now covers attempts, bounded failures, delivery, replay, and dead-letter
through the store abstraction. OpenAPI 0.2 documents invitation and removal
operations. Platform recovery breadth and an outbox operator transport remain.
2026-08-08 follow-up: a provider-neutral `TenantManagementPort` now supports
platform-operator tenant creation with optional first-admin preparation. A
redacted recovery route recreates or reconciles provider identity state and
reactivates tenant lifecycle, while authenticated operator routes dispatch and
replay durable outbox events. Ordinary tenant users are denied these routes.
Broader tenant update/retirement operations remain.
## T02 - Expand self-service onboarding UX
```task
id: USER-WP-0021-T02
status: done
priority: medium
state_hub_task_id: "dc548cfb-db7c-4cbd-864f-2effeebc3dbd"
```
Add invitation acceptance, email-verification status, consent/profile,
tenant-selection, and fully resumable onboarding screens while keeping
password and MFA material on provider-owned surfaces.
2026-08-08 increment: invitation claim is now a first-class authenticated API
operation and preserves the provider-owned credential boundary. Dedicated
browser screens for the fully resumable journey remain.
2026-08-08 browser increment: authenticated users can review and accept an
invitation through a CSRF-protected flow, then land on a responsive onboarding
status screen showing provider verification, tenant memberships, and journey
state. Acceptance binds the verified OIDC email to the invitation address.
Consent/profile editing and tenant switching remain.
2026-08-08 completion increment: self-service profile completion and versioned
consent are now durable, audited, and emitted through the outbox; verified
email remains immutable. Membership-scoped tenant selection is available for
onboarding views, and a PKCE reauthentication handoff carries a tenant hint so
the identity provider—not browser state—changes authoritative tenant context.
Interactive completion of subsystem-owned journey steps remains.
2026-08-08 completion: users can complete active user-engine-owned onboarding
steps through API or CSRF-protected browser controls. Journey ownership is
checked before mutation. Password, MFA, and other subsystem-owned steps expose
handoff status only and cannot be completed from user-engine; their callbacks
resume the durable journey. This completes the self-service onboarding scope.
## T03 - Expand administration UX
```task
id: USER-WP-0021-T03
status: done
priority: high
state_hub_task_id: "3e0b41ee-6159-46a4-a9fe-c5b6d714cc2a"
```
Add platform tenant creation, first-admin bootstrap, invitation/recovery
management, account removal, and redacted lifecycle-gap diagnostics.
2026-08-08 increment: account removal now uses the provider-neutral
deprovisioning port and disables the tenant lifecycle record. Platform
creation/bootstrap and recovery UI remain.
2026-08-08 follow-up: platform APIs now create tenants through the tenant
authority, prepare the first administrator, and perform redacted identity
recovery. Browser administration screens for these operations remain.
2026-08-08 browser increment: tenant administrators can create, inspect,
resend, and expire invitations with versioned CSRF-protected forms, and remove
accounts through the provider-neutral deprovisioning boundary.
2026-08-08 platform-browser increment: platform operators have a responsive,
CSRF-protected tenant-creation screen with optional first-admin preparation
and a direct handoff to tenant administration. Redacted recovery and
lifecycle-gap browser diagnostics remain.
2026-08-08 recovery-browser increment: platform operators can inspect redacted
lifecycle-gap categories and invoke CSRF-protected identity recovery from the
tenant member view. Recovery recreates a missing provider link or reconciles
an existing identity, then restores the tenant lifecycle record without
showing credentials, factor evidence, or provider error bodies.
2026-08-08 completion: tenant and platform browser surfaces now cover every
listed administration operation while retaining tenant boundaries, CSRF,
provider-neutral lifecycle ports, and redacted diagnostics.
## T04 - Complete broad security and accessibility conformance
```task
id: USER-WP-0021-T04
status: done
priority: high
state_hub_task_id: "fb59245e-6989-4bd9-b72a-68e53ea9f0af"
```
Automate duplicate/expired/replayed invitation, session expiry, provider
outage, partial failure, audit-redaction, keyboard/screen-reader, and
mobile/desktop conformance. Preserve existing cross-tenant and escalation
denial gates.
2026-08-08 increment: conformance covers invitation conflicts, resend/expiry,
outbox provider failure, replay, dead-letter, successful recovery, and
provider-neutral account removal. Browser accessibility breadth remains.
2026-08-08 follow-up: conformance now also proves ordinary-user denial of the
platform surface and the positive platform tenant/bootstrap, recovery, and
outbox delivery workflow.
2026-08-08 browser increment: conformance proves CSRF enforcement, invitation
email binding, successful browser claim and onboarding redirect, responsive
viewport metadata, focus-visible controls, semantic landmarks, and invitation
and removal administration controls.
2026-08-08 self-service/recovery increment: conformance additionally covers
durable consent/profile completion, immutable verified email, tenant-hinted
OIDC reauthentication, platform recovery controls, and redacted lifecycle-gap
presentation.
2026-08-08 completion: the automated matrix covers duplicate, explicitly
expired, revoked, and replayed invitations; stale versions; expired sessions;
provider outage and unchanged lifecycle state; bounded error/audit surfaces;
cross-tenant and platform denial; semantic headings/landmarks; visible keyboard
focus; responsive viewport/mobile tables; provider handoff denial; and
desktop/browser positive flows. The full suite passes 115 tests with three
provider integration tests skipped when their external service is absent.
## Upstream corrections consumed (2026-08-18)
tenant-engine reported that production had been silently rolled back to the
pre-lifecycle TEN-WP-0004 image between 2026-08-13 and 2026-08-16, so PATCH,
retire, and reactivate returned 404 while flex-auth simultaneously ran a
four-action policy that would have denied them anyway. Both are resolved and
verified live. Our conformance suite passed throughout because it was right
about the contract and production was not serving it — which is precisely the
gap the handoff note flagged. The live cross-service check from a user-engine
pod is now owed as part of the next rollout; contract-level evidence cannot
see a pin rollback.
tenant-engine also corrected the contract: `grouping` is no longer immutable.
It changes only through the authority's own reclassification route under a
separate `tenant.grouping.set` action, because grouping resolves a tenant's
guardrail spend ceiling and must not ride on a rename. The identifier's
grouping segment is now historical — a tenant created as `tenant:small:acme`
can report grouping `large`. Verified that user-engine performs no identifier
parsing; every `split` in the portal is URL routing. `TenantRecord` had been
dropping the field, so the read now carries it, the operator screen shows it
with the identifier caveat, and OpenAPI documents where to read it. The
reclassification control itself is deliberately not built: the route is not
deployed and wants its own permission.
## T05 - Trigger enterprise federation planning only on demand
```task
id: USER-WP-0021-T05
status: done
priority: low
state_hub_task_id: "05046780-9625-47c2-8caf-f57e9239c603"
```
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
published as
`forgejo.coulomb.social/coulomb/user-engine@sha256:cef2fdb19fd001a9d2a64742f58dacdcf2ff5868ce50685c91eaad7880645fd0`,
and rolled out successfully on Railiance. Public health returns schema
`0001_initial`, KeyCape login returns a PKCE redirect, and `/onboarding`,
`/admin/{tenant}`, and `/platform` are recognized protected routes (403
without a session, rather than 404). The rollout replaces in-memory OIDC
sessions, so existing browsers must authenticate again.
2026-08-09 production follow-up: tenant-engine and its dedicated flex-auth
policy service are deployed with persistent storage and least-privilege
networking. The portal adapter created a tenant successfully and the record
survived a tenant-engine restart. The separate user-engine flex-auth policy is
also deployed and has live self-service-allow/cross-tenant-deny evidence, but
the portal runtime switch remains gated on approved event and mail receivers.
T01 remains open only for broader tenant update/retirement operations and the
final outbox transport activation.
2026-08-10 rollout evidence: the current source image now fails closed when
the required flex-auth and delivery settings are absent. A guarded deployment
was restored to the last known-good revision without service loss. Final T01
activation remains blocked on approved OpenBao audit/mail receiver tokens and
transactional SMTP credentials; no placeholder or reused credential was added.
2026-08-10 authority check: the deployed tenant-engine contract currently
supports tenant creation plus role/plan operations, but exposes no tenant
metadata-update or retirement operation. Portal update/retirement routes must
remain out until that authority owns the corresponding lifecycle contract;
user-engine will not simulate authoritative tenant state locally.
TEN-WP-0005 is now registered and ready in tenant-engine for the authoritative
metadata update, retirement, and reactivation contract. This task is waiting
on that workplan and on the OpenBao-backed event/mail delivery credentials.
The non-secret flex-auth runtime URL is live and verified, removing that item
from the rollout gate.
2026-08-13 delivery hardening: audit-core and email-connect are now live as
separately scoped receivers. `HTTPOutboxDeliveryAdapter` therefore accepts
distinct event and mail bearer tokens; runtime configuration uses
`USER_ENGINE_EVENT_TOKEN` and `USER_ENGINE_MAIL_TOKEN`, and mail delivery
fails closed when its dedicated token is absent. Tests assert that the two
Authorization headers differ. The full suite passes 132 tests with three
external-provider skips. Production rollout still waits on governed delivery
of both scoped credentials.
2026-08-16 tenant lifecycle completion: TEN-WP-0005 is finished, so the last
deferred part of T01 is now implementable against a real authority.
`TenantManagementPort` gained `tenant`, `update_tenant`, `retire_tenant`, and
`reactivate_tenant`, and `HTTPTenantManagementAdapter` implements them against
the `tenant-lifecycle-api.md` contract: version ETag as `If-Match` (never `*`),
`Idempotency-Key`, actor/reason/correlation_id, and `Idempotent-Replay`
surfaced as a `replayed` flag. Authority failures map to redacted domain errors
carrying only the contract's stable `error_code`; the authority's `detail` text
never crosses the boundary.
Platform operators get `GET`/`PATCH /api/v1/platform/tenants/{tenant}` plus
`/retire` and `/reactivate`, and a CSRF-protected browser lifecycle screen that
reads the record first and echoes its version. A retired tenant renders no
metadata form, matching the authority's own transition rules. user-engine keeps
no tenant table and invents no retirement semantics: every read and write goes
to tenant-engine. Portal OpenAPI is now 0.3.0 with `TenantRecord`,
`UpdateTenant`, and `TenantLifecycleChange` schemas, and the immutable
identifier is documented as rejected rather than discovered from a 400.
Conformance proves ordinary-user denial, stale-version 409, replayed mutation
without a second version bump, update-while-retired and double-retirement 409,
reactivation, unknown-tenant 404, forged-CSRF rejection with unchanged
lifecycle, and adapter-level redaction of authority detail text. The full suite
passes 145 tests with three external-provider skips.
This closes the engineering scope of T01. Production activation of the outbox
transport remains gated on governed delivery of the OpenBao event/mail tokens
and transactional SMTP credentials — an operational gate outside this repo, not
outstanding user-engine code.
2026-08-14 live retry finding: the PostgreSQL adapter excluded every row with
`failed_at` set, while the in-memory contract correctly retains failures until
`dead_lettered_at`. A first receiver error therefore required manual replay
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.