generated from coulomb/repo-seed
Implement durable store contract and registration roadmap
This commit is contained in:
@@ -9,7 +9,9 @@ make test
|
||||
See `docs/development.md`, `docs/configuration.md`, `docs/contracts.md`,
|
||||
`docs/canon-mapping.md`, `docs/canon-interface-card.yaml`,
|
||||
`docs/evidence-gap-examples.md`, `docs/family-dataspace-onboarding.md`,
|
||||
`docs/examples.md`, `docs/scenarios.md`,
|
||||
`docs/netkingdom-registration-onboarding-vision.md`,
|
||||
`docs/postgres-durable-store-consumer-requirements.md`, `docs/examples.md`,
|
||||
`docs/scenarios.md`,
|
||||
`docs/operability.md`, `docs/release.md`, `docs/ui-contracts.md`,
|
||||
`docs/identity-domain-naming-decision.md`, and `docs/final-assessment.md`
|
||||
for implementation boundaries, contracts, canon mappings, examples, and release
|
||||
|
||||
9
SCOPE.md
9
SCOPE.md
@@ -42,7 +42,8 @@ application catalogs, projections, evidence references, audit, and events.
|
||||
- policy, control, access-review, exception, and organization source-of-truth
|
||||
ownership;
|
||||
- runtime secret custody;
|
||||
- UI implementation;
|
||||
- UI implementation in the current MVP; optional registration and access
|
||||
management UI work is proposed separately under `USER-WP-0014`;
|
||||
- full SCIM server or enterprise directory replacement in the initial product.
|
||||
|
||||
## Boundary Rule
|
||||
@@ -56,5 +57,7 @@ truth.
|
||||
|
||||
## Current Planning
|
||||
|
||||
Implementation work is tracked in `workplans/USER-WP-0001` through
|
||||
`USER-WP-0006`.
|
||||
Implementation and planning work is tracked in `workplans/USER-WP-0001`
|
||||
through `USER-WP-0015`. `USER-WP-0010` through `USER-WP-0015` are proposed
|
||||
future workplans for NetKingdom registration, prepared accounts, hats/access
|
||||
profiles, onboarding journeys, optional UI, and security conformance.
|
||||
|
||||
@@ -89,6 +89,24 @@ without emitting outbox events.
|
||||
Local audit records may be exported as identity-canon `Evidence Source`
|
||||
references. Durable platform audit custody remains outside user-engine.
|
||||
|
||||
## Durable Store Contract
|
||||
|
||||
`UserEngineService` depends on the `UserEngineStore` protocol, not the
|
||||
in-memory adapter's concrete collections. Store implementations must expose
|
||||
schema readiness, logical record accessors, audit-log reads, pending-outbox
|
||||
reads, adapter-neutral record counts, and a `transaction` context for atomic
|
||||
mutations.
|
||||
|
||||
Mutating writes happen after validation and authorization, inside the store
|
||||
transaction. Domain changes, local mutation audit records, and outbox events
|
||||
must commit or roll back together. Authorization-denial audit records must
|
||||
remain durable without outbox events, including when a denial occurs inside a
|
||||
composed mutation that rolls back other writes.
|
||||
|
||||
Postgres-specific connection handling, SQL, locks, credentials, tenant
|
||||
isolation primitives, backup, restore, and platform observability remain
|
||||
adapter or provider concerns outside the domain service.
|
||||
|
||||
## Migration Contract
|
||||
|
||||
The isolated store exposes `SCHEMA_VERSION = 0001_initial` and a `migrate`
|
||||
|
||||
259
docs/netkingdom-registration-onboarding-vision.md
Normal file
259
docs/netkingdom-registration-onboarding-vision.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# NetKingdom Registration And Onboarding Vision
|
||||
|
||||
Status: vision and proposed roadmap
|
||||
Date: 2026-06-15
|
||||
Related workplans: USER-WP-0010 through USER-WP-0015
|
||||
|
||||
## Purpose
|
||||
|
||||
NetKingdom needs a convenient way for people to register, receive a stable
|
||||
NetKingdom identity, claim prepared rights, choose the role or "hat" they are
|
||||
acting under, and enter services through guided onboarding journeys.
|
||||
|
||||
`user-engine` can support this as the identity-domain and user-domain system
|
||||
behind a registration UI. It should not become the identity provider,
|
||||
credential system, MFA provider, final authorization policy engine, or runtime
|
||||
ACL enforcer. Instead, it should provide the domain model, orchestration
|
||||
facades, profile and membership facts, identity context, audit/outbox events,
|
||||
and optional UI/API contracts that NetKingdom IAM, authorization, and service
|
||||
runtimes can consume.
|
||||
|
||||
## Product Vision
|
||||
|
||||
A new user should be able to arrive at NetKingdom, establish the required
|
||||
identity factors, receive or claim a NetKingdom ID, and immediately see the
|
||||
services, realms, groups, and assets they may access. If the user is expected
|
||||
before registration, an administrator, tenant owner, family owner, or upstream
|
||||
system should be able to prepare the account and rights in advance. During
|
||||
registration, verified factors such as email, phone, postal address, or eID can
|
||||
match those preparations and attach the waiting access package to the new
|
||||
account.
|
||||
|
||||
The end state is:
|
||||
|
||||
```text
|
||||
People register once with NetKingdom.
|
||||
NetKingdom links verified factors and external identities to one canonical user.
|
||||
Prepared rights can be claimed safely when factor evidence matches.
|
||||
Users choose an active hat for the context they are entering.
|
||||
Applications receive claims, profile projections, and membership facts.
|
||||
Authorization systems evaluate ACLs and policy from explicit facts.
|
||||
Subsystem onboarding is driven by events, welcome protocols, and journeys.
|
||||
```
|
||||
|
||||
## Answer: Can user-engine Provide A UI?
|
||||
|
||||
Yes, but the UI should be an optional NetKingdom registration and onboarding
|
||||
surface backed by user-engine service contracts. The repo's current intent is
|
||||
"headless first" and "optional UI, not UI-driven". That means user-engine can
|
||||
own a small registration UI or UI contract when it is the most convenient way
|
||||
to operate the domain, as long as source-of-truth boundaries stay explicit:
|
||||
|
||||
- NetKingdom IAM verifies credentials and proofing factors.
|
||||
- user-engine stores users, accounts, identity links, memberships, profiles,
|
||||
prepared-account claims, role context, and onboarding state.
|
||||
- Authorization systems make final access decisions.
|
||||
- Service runtimes enforce ACLs for their own resources.
|
||||
- Audit, evidence, and lifecycle systems receive exported records and events.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### NetKingdom ID
|
||||
|
||||
The NetKingdom ID should be a stable canonical identifier for the person in the
|
||||
NetKingdom identity domain. It should not expose raw identity-provider
|
||||
issuer/subject pairs. user-engine can mint or map this identifier through the
|
||||
`User` record and `ExternalIdentity` links, while IAM continues to authenticate
|
||||
the subject.
|
||||
|
||||
### Registration Session
|
||||
|
||||
A registration session is a short-lived onboarding workflow. It tracks the
|
||||
actor, verified factor evidence, selected tenant or realm context, consent,
|
||||
prepared-account matches, requested roles, and completion state. It should be
|
||||
auditable and resumable without storing secret credential material.
|
||||
|
||||
### Identity Factors
|
||||
|
||||
Factors are evidence that help establish or link identity:
|
||||
|
||||
- email address;
|
||||
- phone number;
|
||||
- postal address;
|
||||
- eID or government-backed identity;
|
||||
- organization-issued invite;
|
||||
- existing SSO identity;
|
||||
- recovery or delegated caretaker evidence.
|
||||
|
||||
user-engine should store factor references, verification status, assurance
|
||||
level, expiry, source system, and evidence references. It should not perform
|
||||
the proofing itself unless a later adapter explicitly owns a local development
|
||||
mock.
|
||||
|
||||
### Prepared Accounts And Rights
|
||||
|
||||
Prepared accounts allow NetKingdom to create pending user-account intent before
|
||||
the person registers. A prepared account can contain:
|
||||
|
||||
- expected factor match rules;
|
||||
- tenant, group, family, realm, service, or asset scope;
|
||||
- initial account state;
|
||||
- role or hat templates;
|
||||
- profile defaults;
|
||||
- customer-journey steps;
|
||||
- approval requirements;
|
||||
- expiry and revocation rules.
|
||||
|
||||
When a registering user proves the required factors, user-engine can link the
|
||||
prepared account to the real user and convert prepared rights into explicit
|
||||
memberships, profile values, and onboarding tasks.
|
||||
|
||||
### Hats, Roles, And Profiles
|
||||
|
||||
A "hat" is the active context a user chooses when entering NetKingdom or a
|
||||
service. Examples include family owner, child, tenant admin, employee,
|
||||
contractor, service operator, customer, vendor, or agent delegate.
|
||||
|
||||
In user-engine, hats should be represented through tenant-scoped memberships,
|
||||
role labels, profile layers, and application projections. A user may have many
|
||||
hats, but only a subset should be active in a given realm, service, or asset
|
||||
context.
|
||||
|
||||
### Realms, Services, Assets, And ACLs
|
||||
|
||||
user-engine should efficiently manage identity-domain access facts for users
|
||||
and groups against realms, services, and assets. It should not become the final
|
||||
ACL enforcement engine. The recommended split is:
|
||||
|
||||
- user-engine owns user, group, membership, role, profile, and access-intent
|
||||
facts;
|
||||
- authorization systems evaluate policy and ACLs from those facts;
|
||||
- services enforce decisions at runtime;
|
||||
- user-engine exports identity context and claims-enrichment projections for
|
||||
the active hat.
|
||||
|
||||
## Target Flows
|
||||
|
||||
### Self Registration
|
||||
|
||||
1. User opens the NetKingdom registration UI.
|
||||
2. IAM verifies one or more required factors.
|
||||
3. user-engine creates or resolves the canonical user and account.
|
||||
4. user-engine links verified external identities and factor evidence.
|
||||
5. user-engine evaluates prepared-account matches.
|
||||
6. User accepts terms, chooses initial tenant or realm, and selects available
|
||||
hats.
|
||||
7. user-engine emits audit and outbox events for downstream onboarding.
|
||||
8. Services receive identity context and claims projections.
|
||||
|
||||
### Prepared Account Claim
|
||||
|
||||
1. Admin, family owner, tenant admin, HR feed, service owner, or invite source
|
||||
prepares an account package.
|
||||
2. The package declares required factor matches and planned roles/profiles.
|
||||
3. User registers and proves the required factors.
|
||||
4. user-engine links the preparation to the canonical user.
|
||||
5. Prepared memberships, profile defaults, and onboarding tasks are activated.
|
||||
6. Any sensitive or privileged role waits for approval if policy requires it.
|
||||
|
||||
### Hat Selection
|
||||
|
||||
1. User signs in and sees available hats for the current tenant, realm, or
|
||||
service.
|
||||
2. User selects an active hat.
|
||||
3. user-engine returns `identity_context` and a claims-enrichment projection
|
||||
for that context.
|
||||
4. IAM or a gateway issues service-facing claims.
|
||||
5. Authorization and service runtimes evaluate ACLs and policy.
|
||||
|
||||
### Welcome Protocols
|
||||
|
||||
1. Registration or prepared-account claim emits onboarding events.
|
||||
2. Journey definitions map events to subsystem steps.
|
||||
3. Welcome protocols send the user to profile completion, family setup, tenant
|
||||
selection, app tour, evidence collection, approval, or service activation.
|
||||
4. Each subsystem reports completion, failure, or required manual follow-up.
|
||||
|
||||
## UI Surface
|
||||
|
||||
The first UI should be functional, quiet, and workflow-oriented. It should
|
||||
support:
|
||||
|
||||
- registration start and resume;
|
||||
- factor verification status;
|
||||
- prepared-account claim review;
|
||||
- terms and consent capture;
|
||||
- role or hat selection;
|
||||
- profile completion;
|
||||
- welcome journey timeline;
|
||||
- access request and pending approval status;
|
||||
- administrator views for prepared accounts, invitations, groups, realms,
|
||||
service bindings, and onboarding diagnostics.
|
||||
|
||||
The UI should call stable service/application APIs. It should not embed IAM,
|
||||
authorization, proofing, or service-specific ACL logic in browser code.
|
||||
|
||||
## Domain Additions Needed
|
||||
|
||||
The current domain already has users, accounts, tenant accounts, external
|
||||
identities, memberships, profiles, applications, catalogs, invitations, audit,
|
||||
outbox, and identity context. The registration vision needs additional
|
||||
concepts:
|
||||
|
||||
- `RegistrationSession`
|
||||
- `NetKingdomIdentity` or public NetKingdom ID alias
|
||||
- `IdentityFactor`
|
||||
- `FactorVerification`
|
||||
- `PreparedAccount`
|
||||
- `PreparedEntitlement`
|
||||
- `Hat` or active role context
|
||||
- `Realm`
|
||||
- `ServiceArea`
|
||||
- `AssetScope`
|
||||
- `AccessProfile`
|
||||
- `AccessIntent`
|
||||
- `OnboardingJourney`
|
||||
- `WelcomeProtocol`
|
||||
- `OnboardingTask`
|
||||
|
||||
These should be introduced incrementally through workplans rather than all at
|
||||
once.
|
||||
|
||||
## Security And Governance
|
||||
|
||||
- Factor values must be minimized, normalized, and redacted in diagnostics.
|
||||
- High-assurance factors such as eID should be represented by evidence
|
||||
references and assurance metadata, not raw proofing payloads.
|
||||
- Prepared rights must expire, be revocable, and show who prepared them.
|
||||
- Privileged hats require explicit evidence, approval, or policy/control
|
||||
references.
|
||||
- Users should see why a prepared account or role is available before claiming
|
||||
it.
|
||||
- Access to realms, services, and assets must fail closed when tenant, hat, or
|
||||
factor context is missing.
|
||||
- All lifecycle transitions should be auditable and emit outbox events.
|
||||
|
||||
## Recommended Workplans
|
||||
|
||||
| Workplan | Title | Purpose |
|
||||
| --- | --- | --- |
|
||||
| USER-WP-0010 | Registration Identity And Factor Model | Add registration sessions, NetKingdom ID semantics, factor evidence, and verification adapter boundaries. |
|
||||
| USER-WP-0011 | Prepared Accounts And Entitlement Claims | Allow accounts, roles, profiles, and journeys to be prepared before registration and claimed after factor match. |
|
||||
| USER-WP-0012 | Hats, Realms, Services, Assets, And Access Profiles | Model active hats and access-control facts for users/groups across realms, services, and assets. |
|
||||
| USER-WP-0013 | Onboarding Journeys And Welcome Protocols | Orchestrate subsystem welcome flows from registration, invitation, and prepared-account events. |
|
||||
| USER-WP-0014 | Registration And Access Management UI | Build the optional UI/API surface for registration, factor status, prepared rights, hat selection, and admin setup. |
|
||||
| USER-WP-0015 | Registration Scenario And Security Conformance | Add end-to-end scenarios, threat-oriented negative tests, redaction checks, and adapter conformance for the full flow. |
|
||||
|
||||
## First Milestone
|
||||
|
||||
The first useful milestone should not be the full UI. It should be a headless
|
||||
registration facade and scenario tests:
|
||||
|
||||
```text
|
||||
Start registration -> verify email through adapter evidence -> create
|
||||
NetKingdom user/account -> claim a prepared tenant role -> choose active hat ->
|
||||
return identity_context and claims projection -> emit onboarding events.
|
||||
```
|
||||
|
||||
After that is stable, a thin UI can use the same facade without inventing its
|
||||
own registration rules.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Postgres Durable Store Consumer Requirements
|
||||
|
||||
Status: requirements
|
||||
Date: 2026-06-05
|
||||
Status: requirements + store contract boundary
|
||||
Date: 2026-06-15
|
||||
Related workplan: USER-WP-0009
|
||||
|
||||
## Purpose
|
||||
@@ -13,6 +13,12 @@ NetKingdom infrastructure repository provides a tenant-aware, security
|
||||
integrated Postgres capability, and `user-engine` consumes that capability
|
||||
through a durable store adapter.
|
||||
|
||||
The consumer-side contract is now represented in code by
|
||||
`user_engine.ports.UserEngineStore`. The protocol is intentionally
|
||||
adapter-neutral: it names the service behavior a durable store must satisfy
|
||||
without adding a Postgres dependency or giving this repository ownership of
|
||||
database provisioning.
|
||||
|
||||
## Consumer Story
|
||||
|
||||
As a `user-engine` consumer, I want the service to persist identity-domain
|
||||
@@ -85,6 +91,22 @@ the isolated store:
|
||||
- Support the same service-level exceptions for not found, conflict,
|
||||
validation, and authorization-denied flows.
|
||||
|
||||
### Store Protocol Boundary
|
||||
|
||||
`UserEngineService` consumes the `UserEngineStore` protocol rather than local
|
||||
in-memory collections. A future Postgres adapter must provide:
|
||||
|
||||
- Schema readiness through `schema_version`, `ready`, and `migrate`.
|
||||
- A `transaction` context that makes each mutating write unit atomic.
|
||||
- Logical read/write methods for users, accounts, tenant accounts, external
|
||||
identities, memberships, applications, bindings, catalogs, family
|
||||
invitations, and profile values.
|
||||
- Audit and outbox append/read methods that preserve write order.
|
||||
- Adapter-neutral record counts for diagnostics and operability snapshots.
|
||||
|
||||
Concrete tables, SQL, connection pools, and row locks remain adapter details.
|
||||
Service and domain code should not depend on Postgres-specific concepts.
|
||||
|
||||
### Identity And Account Constraints
|
||||
|
||||
- `(issuer, subject)` must uniquely identify one external identity link.
|
||||
@@ -157,6 +179,9 @@ the isolated store:
|
||||
them to `ConflictError` where appropriate.
|
||||
- Migration and outbox claiming should use explicit locking strategies that do
|
||||
not require consumers to understand Postgres internals.
|
||||
- Authorization-denial audit records must persist without outbox events even
|
||||
when the denied operation occurs inside a composed transaction that rolls
|
||||
back domain writes.
|
||||
|
||||
### Migration Requirements
|
||||
|
||||
@@ -253,10 +278,16 @@ A future Postgres adapter should pass conformance tests for:
|
||||
|
||||
## First Implementation Follow-Ups
|
||||
|
||||
After this requirements work is accepted, likely follow-up work should be:
|
||||
The first consumer-side follow-up is complete: `UserEngineStore` defines the
|
||||
adapter boundary and the in-memory store acts as the reference implementation
|
||||
for service-level behavior.
|
||||
|
||||
Likely future follow-up work should be:
|
||||
|
||||
- Define the durable store protocol changes, if any.
|
||||
- Add a Postgres adapter behind the existing store boundary.
|
||||
- Add migration files for user-engine tables.
|
||||
- Add provider-backed conformance tests for locking, uniqueness races,
|
||||
migration readiness, outbox claiming, redacted diagnostics, and restore
|
||||
validation.
|
||||
- Add conformance tests that run against both in-memory and Postgres stores.
|
||||
- Integrate the adapter with the future NetKingdom Postgres provider repo.
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Iterable
|
||||
from typing import Iterable, Iterator, Mapping, cast
|
||||
|
||||
from user_engine.domain import (
|
||||
Account,
|
||||
@@ -51,6 +53,10 @@ class InMemoryUserEngineStore:
|
||||
] = field(default_factory=dict)
|
||||
audit_records: list[AuditRecord] = field(default_factory=list)
|
||||
outbox_events: list[OutboxEvent] = field(default_factory=list)
|
||||
_transaction_depth: int = field(default=0, init=False, repr=False)
|
||||
_transaction_snapshot: Mapping[str, object] | None = field(
|
||||
default=None, init=False, repr=False
|
||||
)
|
||||
|
||||
def migrate(self) -> None:
|
||||
"""Apply the standalone schema migration manifest."""
|
||||
@@ -60,15 +66,42 @@ class InMemoryUserEngineStore:
|
||||
def ready(self) -> bool:
|
||||
return self.schema_version == SCHEMA_VERSION
|
||||
|
||||
@contextmanager
|
||||
def transaction(self) -> Iterator[None]:
|
||||
"""Provide atomic in-memory mutation semantics for conformance tests."""
|
||||
if self._transaction_depth == 0:
|
||||
self._transaction_snapshot = self._snapshot()
|
||||
self._transaction_depth += 1
|
||||
try:
|
||||
yield
|
||||
except Exception:
|
||||
if self._transaction_depth == 1 and self._transaction_snapshot is not None:
|
||||
self._restore(self._transaction_snapshot)
|
||||
raise
|
||||
finally:
|
||||
self._transaction_depth -= 1
|
||||
if self._transaction_depth == 0:
|
||||
self._transaction_snapshot = None
|
||||
|
||||
def save_user(self, user: User) -> None:
|
||||
self.users[user.user_id] = user
|
||||
|
||||
def user(self, user_id: str) -> User | None:
|
||||
return self.users.get(user_id)
|
||||
|
||||
def save_account(self, account: Account) -> None:
|
||||
self.accounts[account.user_id] = account
|
||||
|
||||
def save_identity(self, identity: ExternalIdentity) -> None:
|
||||
self.identities[identity.identity_key] = identity
|
||||
|
||||
def identities_for_user(self, user_id: str) -> tuple[ExternalIdentity, ...]:
|
||||
return tuple(
|
||||
identity
|
||||
for identity in self.identities.values()
|
||||
if identity.user_id == user_id
|
||||
)
|
||||
|
||||
def save_tenant_account(self, account: TenantAccount) -> None:
|
||||
self.tenant_accounts[(account.tenant, account.user_id)] = account
|
||||
|
||||
@@ -78,12 +111,24 @@ class InMemoryUserEngineStore:
|
||||
def save_application(self, application: Application) -> None:
|
||||
self.applications[application.application_id] = application
|
||||
|
||||
def application(self, application_id: str) -> Application | None:
|
||||
return self.applications.get(application_id)
|
||||
|
||||
def save_binding(self, binding: ApplicationBinding) -> None:
|
||||
self.bindings[binding.application_id] = binding
|
||||
|
||||
def binding(self, application_id: str) -> ApplicationBinding | None:
|
||||
return self.bindings.get(application_id)
|
||||
|
||||
def save_catalog(self, catalog: Catalog) -> None:
|
||||
self.catalogs[catalog.catalog_id] = catalog
|
||||
|
||||
def catalog(self, catalog_id: str) -> Catalog | None:
|
||||
return self.catalogs.get(catalog_id)
|
||||
|
||||
def all_catalogs(self) -> tuple[Catalog, ...]:
|
||||
return tuple(self.catalogs.values())
|
||||
|
||||
def save_family_invitation(self, invitation: FamilyInvitation) -> None:
|
||||
self.family_invitations[invitation.invitation_id] = invitation
|
||||
|
||||
@@ -136,9 +181,67 @@ class InMemoryUserEngineStore:
|
||||
def append_audit(self, record: AuditRecord) -> None:
|
||||
self.audit_records.append(record)
|
||||
|
||||
def audit_log(self) -> tuple[AuditRecord, ...]:
|
||||
return tuple(self.audit_records)
|
||||
|
||||
def append_outbox(self, event: OutboxEvent) -> None:
|
||||
self.outbox_events.append(event)
|
||||
|
||||
def pending_outbox(self) -> tuple[OutboxEvent, ...]:
|
||||
return tuple(self.outbox_events)
|
||||
|
||||
def record_counts(self) -> Mapping[str, int]:
|
||||
return {
|
||||
"users": len(self.users),
|
||||
"accounts": len(self.accounts),
|
||||
"tenant_accounts": len(self.tenant_accounts),
|
||||
"memberships": len(self.memberships),
|
||||
"applications": len(self.applications),
|
||||
"catalogs": len(self.catalogs),
|
||||
"family_invitations": len(self.family_invitations),
|
||||
"profile_values": len(self.profile_values),
|
||||
"audit_records": len(self.audit_records),
|
||||
"pending_outbox_events": len(self.outbox_events),
|
||||
}
|
||||
|
||||
def _snapshot(self) -> Mapping[str, object]:
|
||||
return {
|
||||
"users": copy.deepcopy(self.users),
|
||||
"accounts": copy.deepcopy(self.accounts),
|
||||
"identities": copy.deepcopy(self.identities),
|
||||
"tenant_accounts": copy.deepcopy(self.tenant_accounts),
|
||||
"memberships": copy.deepcopy(self.memberships),
|
||||
"applications": copy.deepcopy(self.applications),
|
||||
"bindings": copy.deepcopy(self.bindings),
|
||||
"catalogs": copy.deepcopy(self.catalogs),
|
||||
"family_invitations": copy.deepcopy(self.family_invitations),
|
||||
"profile_values": copy.deepcopy(self.profile_values),
|
||||
"audit_records": copy.deepcopy(self.audit_records),
|
||||
"outbox_events": copy.deepcopy(self.outbox_events),
|
||||
}
|
||||
|
||||
def _restore(self, snapshot: Mapping[str, object]) -> None:
|
||||
snapshot_audit_records = cast(list[AuditRecord], snapshot["audit_records"])
|
||||
denied_audit_records = [
|
||||
record
|
||||
for record in self.audit_records[len(snapshot_audit_records) :]
|
||||
if record.summary == "authorization denied"
|
||||
]
|
||||
self.users = snapshot["users"] # type: ignore[assignment]
|
||||
self.accounts = snapshot["accounts"] # type: ignore[assignment]
|
||||
self.identities = snapshot["identities"] # type: ignore[assignment]
|
||||
self.tenant_accounts = snapshot["tenant_accounts"] # type: ignore[assignment]
|
||||
self.memberships = snapshot["memberships"] # type: ignore[assignment]
|
||||
self.applications = snapshot["applications"] # type: ignore[assignment]
|
||||
self.bindings = snapshot["bindings"] # type: ignore[assignment]
|
||||
self.catalogs = snapshot["catalogs"] # type: ignore[assignment]
|
||||
self.family_invitations = snapshot[
|
||||
"family_invitations"
|
||||
] # type: ignore[assignment]
|
||||
self.profile_values = snapshot["profile_values"] # type: ignore[assignment]
|
||||
self.audit_records = [*snapshot_audit_records, *denied_audit_records]
|
||||
self.outbox_events = snapshot["outbox_events"] # type: ignore[assignment]
|
||||
|
||||
|
||||
class LocalAuthorizationCheckPort:
|
||||
"""Deterministic local authorization adapter.
|
||||
|
||||
@@ -7,20 +7,141 @@ adapters without changing domain code.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager
|
||||
from typing import Any, Iterable, Mapping, Protocol
|
||||
|
||||
from user_engine.domain import (
|
||||
Account,
|
||||
Actor,
|
||||
Application,
|
||||
ApplicationBinding,
|
||||
AuditRecord,
|
||||
AuthorizationDecision,
|
||||
AuthorizationRequest,
|
||||
CanonEntityReference,
|
||||
Catalog,
|
||||
ExternalIdentity,
|
||||
FamilyInvitation,
|
||||
Membership,
|
||||
OutboxEvent,
|
||||
ProfileValue,
|
||||
TenantAccount,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class UserEngineStore(Protocol):
|
||||
"""Durable persistence boundary for user-engine service behavior.
|
||||
|
||||
Implementations may be in-memory, Postgres-backed, or platform-provided,
|
||||
but must preserve the same logical keys, readiness contract, and atomic
|
||||
mutation semantics exposed here.
|
||||
"""
|
||||
|
||||
schema_version: str | None
|
||||
|
||||
@property
|
||||
def ready(self) -> bool:
|
||||
"""Return whether the store is schema-compatible for service use."""
|
||||
|
||||
def migrate(self) -> None:
|
||||
"""Apply or verify user-engine-owned schema migrations."""
|
||||
|
||||
def transaction(self) -> AbstractContextManager[None]:
|
||||
"""Return a context manager for one atomic mutation unit."""
|
||||
|
||||
def save_user(self, user: User) -> None:
|
||||
"""Create or replace a user record."""
|
||||
|
||||
def user(self, user_id: str) -> User | None:
|
||||
"""Return a user by id."""
|
||||
|
||||
def save_account(self, account: Account) -> None:
|
||||
"""Create or replace a primary account record."""
|
||||
|
||||
def user_account(self, user_id: str) -> Account | None:
|
||||
"""Return the primary account for a user."""
|
||||
|
||||
def save_identity(self, identity: ExternalIdentity) -> None:
|
||||
"""Create or replace an external identity link."""
|
||||
|
||||
def find_identity(self, issuer: str, subject: str) -> ExternalIdentity | None:
|
||||
"""Return an external identity by issuer and subject."""
|
||||
|
||||
def identities_for_user(self, user_id: str) -> tuple[ExternalIdentity, ...]:
|
||||
"""Return all external identities linked to a user."""
|
||||
|
||||
def save_tenant_account(self, account: TenantAccount) -> None:
|
||||
"""Create or replace a tenant-scoped account record."""
|
||||
|
||||
def tenant_account(self, tenant: str, user_id: str) -> TenantAccount | None:
|
||||
"""Return a tenant-scoped account record."""
|
||||
|
||||
def save_membership(self, membership: Membership) -> None:
|
||||
"""Create or replace a membership fact."""
|
||||
|
||||
def memberships_for_user(
|
||||
self, user_id: str, *, tenant: str | None = None
|
||||
) -> tuple[Membership, ...]:
|
||||
"""Return memberships for a user, optionally scoped to a tenant."""
|
||||
|
||||
def memberships_for_tenant(self, tenant: str) -> tuple[Membership, ...]:
|
||||
"""Return memberships scoped to a tenant."""
|
||||
|
||||
def save_application(self, application: Application) -> None:
|
||||
"""Create or replace an application registration."""
|
||||
|
||||
def application(self, application_id: str) -> Application | None:
|
||||
"""Return an application by id."""
|
||||
|
||||
def save_binding(self, binding: ApplicationBinding) -> None:
|
||||
"""Create or replace an application binding."""
|
||||
|
||||
def binding(self, application_id: str) -> ApplicationBinding | None:
|
||||
"""Return an application binding by application id."""
|
||||
|
||||
def save_catalog(self, catalog: Catalog) -> None:
|
||||
"""Create or replace a catalog."""
|
||||
|
||||
def catalog(self, catalog_id: str) -> Catalog | None:
|
||||
"""Return a catalog by id."""
|
||||
|
||||
def all_catalogs(self) -> tuple[Catalog, ...]:
|
||||
"""Return all catalogs."""
|
||||
|
||||
def save_family_invitation(self, invitation: FamilyInvitation) -> None:
|
||||
"""Create or replace a family invitation."""
|
||||
|
||||
def family_invitation(self, invitation_id: str) -> FamilyInvitation | None:
|
||||
"""Return a family invitation by id."""
|
||||
|
||||
def family_invitations_for_user(
|
||||
self, user_id: str
|
||||
) -> tuple[FamilyInvitation, ...]:
|
||||
"""Return family invitations for a user."""
|
||||
|
||||
def save_profile_value(self, value: ProfileValue) -> None:
|
||||
"""Create or replace a profile value."""
|
||||
|
||||
def values_for_user(self, user_id: str) -> tuple[ProfileValue, ...]:
|
||||
"""Return profile values for a user."""
|
||||
|
||||
def append_audit(self, record: AuditRecord) -> None:
|
||||
"""Append a local audit record."""
|
||||
|
||||
def audit_log(self) -> tuple[AuditRecord, ...]:
|
||||
"""Return local audit records in write order."""
|
||||
|
||||
def append_outbox(self, event: OutboxEvent) -> None:
|
||||
"""Append an outbox event."""
|
||||
|
||||
def pending_outbox(self) -> tuple[OutboxEvent, ...]:
|
||||
"""Return pending outbox events in write order."""
|
||||
|
||||
def record_counts(self) -> Mapping[str, int]:
|
||||
"""Return adapter-neutral record counts for diagnostics."""
|
||||
|
||||
|
||||
class IdentityClaimsAdapter(Protocol):
|
||||
"""Normalize verified identity claims into a user-engine actor."""
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,26 @@
|
||||
import unittest
|
||||
from typing import Any
|
||||
|
||||
from user_engine.domain import AuthorizationEffect, AuthorizationRequest
|
||||
from user_engine.adapters.local import (
|
||||
InMemoryUserEngineStore,
|
||||
LocalAuthorizationCheckPort,
|
||||
)
|
||||
from user_engine.domain import (
|
||||
AuthorizationEffect,
|
||||
AuthorizationRequest,
|
||||
OutboxEvent,
|
||||
ProfileScope,
|
||||
ProjectionType,
|
||||
)
|
||||
from user_engine.errors import AuthorizationDenied
|
||||
from user_engine.service import UserEngineService
|
||||
from user_engine.testing.fixtures import (
|
||||
FixtureIdentityClaimsAdapter,
|
||||
StaticAuthorizationCheckPort,
|
||||
human_actor_claims,
|
||||
sample_application,
|
||||
sample_application_binding,
|
||||
sample_catalog,
|
||||
)
|
||||
|
||||
|
||||
@@ -44,6 +59,127 @@ class PortFixtureTests(unittest.TestCase):
|
||||
self.assertEqual(binding.oidc_client_id, "demo-client")
|
||||
self.assertEqual(binding.protected_system_id, "user-engine.demo")
|
||||
|
||||
def test_user_engine_service_consumes_store_protocol(self):
|
||||
store = _ProtocolOnlyStore(InMemoryUserEngineStore())
|
||||
service = UserEngineService(
|
||||
store=store,
|
||||
identity_adapter=FixtureIdentityClaimsAdapter(),
|
||||
authorization=StaticAuthorizationCheckPort(),
|
||||
)
|
||||
|
||||
session = service.me(human_actor_claims(), correlation_id="corr-me")
|
||||
service.register_application(
|
||||
session.actor,
|
||||
sample_application(),
|
||||
binding=sample_application_binding(),
|
||||
correlation_id="corr-app",
|
||||
)
|
||||
service.publish_catalog(
|
||||
session.actor,
|
||||
sample_catalog(),
|
||||
correlation_id="corr-catalog",
|
||||
)
|
||||
service.set_profile_value(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
"demo.display_density",
|
||||
"compact",
|
||||
scope=ProfileScope.APPLICATION,
|
||||
scope_id="app.demo",
|
||||
application_id="app.demo",
|
||||
correlation_id="corr-profile",
|
||||
)
|
||||
projection = service.projection(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
ProjectionType.APPLICATION_RUNTIME,
|
||||
application_id="app.demo",
|
||||
correlation_id="corr-projection",
|
||||
)
|
||||
|
||||
self.assertEqual(projection.values["demo.display_density"], "compact")
|
||||
self.assertTrue(service.operability_snapshot().ready)
|
||||
|
||||
def test_store_transaction_rolls_back_failed_mutation(self):
|
||||
store = _FailingOutboxStore()
|
||||
service = UserEngineService(
|
||||
store=store,
|
||||
identity_adapter=FixtureIdentityClaimsAdapter(),
|
||||
authorization=StaticAuthorizationCheckPort(),
|
||||
)
|
||||
|
||||
with self.assertRaises(RuntimeError):
|
||||
service.me(human_actor_claims(), correlation_id="corr-fail")
|
||||
|
||||
self.assertEqual(store.record_counts()["users"], 0)
|
||||
self.assertEqual(store.audit_log(), ())
|
||||
self.assertEqual(store.pending_outbox(), ())
|
||||
self.assertIsNone(
|
||||
store.find_identity("https://issuer.example.test", "user-123")
|
||||
)
|
||||
|
||||
def test_denial_audit_survives_outer_transaction_rollback(self):
|
||||
store = InMemoryUserEngineStore()
|
||||
service = UserEngineService(
|
||||
store=store,
|
||||
identity_adapter=FixtureIdentityClaimsAdapter(),
|
||||
authorization=LocalAuthorizationCheckPort(
|
||||
action_effects={"membership.write": AuthorizationEffect.DENY}
|
||||
),
|
||||
)
|
||||
session = service.me(human_actor_claims(), correlation_id="corr-me")
|
||||
before_audit = len(store.audit_log())
|
||||
before_outbox = len(store.pending_outbox())
|
||||
|
||||
with self.assertRaises(AuthorizationDenied):
|
||||
with store.transaction():
|
||||
service.add_membership(
|
||||
session.actor,
|
||||
session.user.user_id,
|
||||
tenant="tenant:coulomb",
|
||||
scope_type="team",
|
||||
scope_id="team:demo",
|
||||
kind="member",
|
||||
correlation_id="corr-denied-membership",
|
||||
)
|
||||
|
||||
self.assertEqual(len(store.audit_log()), before_audit + 1)
|
||||
self.assertEqual(store.audit_log()[-1].summary, "authorization denied")
|
||||
self.assertEqual(len(store.pending_outbox()), before_outbox)
|
||||
self.assertEqual(store.record_counts()["memberships"], 0)
|
||||
|
||||
|
||||
class _ProtocolOnlyStore:
|
||||
"""Proxy that fails if service code reaches for local collection fields."""
|
||||
|
||||
_blocked_fields = {
|
||||
"accounts",
|
||||
"applications",
|
||||
"audit_records",
|
||||
"bindings",
|
||||
"catalogs",
|
||||
"family_invitations",
|
||||
"identities",
|
||||
"memberships",
|
||||
"outbox_events",
|
||||
"profile_values",
|
||||
"tenant_accounts",
|
||||
"users",
|
||||
}
|
||||
|
||||
def __init__(self, inner: InMemoryUserEngineStore) -> None:
|
||||
self._inner = inner
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
if name in self._blocked_fields:
|
||||
raise AssertionError(f"service accessed concrete store field {name}")
|
||||
return getattr(self._inner, name)
|
||||
|
||||
|
||||
class _FailingOutboxStore(InMemoryUserEngineStore):
|
||||
def append_outbox(self, event: OutboxEvent) -> None:
|
||||
raise RuntimeError("outbox unavailable")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -4,13 +4,13 @@ type: workplan
|
||||
title: "Postgres Durable Store Consumer Requirements"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 9
|
||||
created: "2026-06-05"
|
||||
updated: "2026-06-05"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0007
|
||||
state_hub_workstream_id: "b5c85993-4aa2-4a8d-98b6-d174ab1b4538"
|
||||
@@ -22,9 +22,11 @@ state_hub_workstream_id: "b5c85993-4aa2-4a8d-98b6-d174ab1b4538"
|
||||
|
||||
Define, from the `user-engine` consumer perspective, what a durable
|
||||
Postgres-backed store must provide before user-engine depends on it in
|
||||
NetKingdom. This workplan is requirements-only: it should not implement the
|
||||
Postgres adapter, provision databases, create tenant infrastructure, or choose
|
||||
the final provider repository design.
|
||||
NetKingdom. The 2026-06-15 review also identified and closed one missing
|
||||
durable-store contract in this repository: `UserEngineService` now consumes an
|
||||
adapter-neutral store protocol instead of the concrete in-memory store. This
|
||||
workplan still does not implement the Postgres adapter, provision databases,
|
||||
create tenant infrastructure, or choose the final provider repository design.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
@@ -51,7 +53,7 @@ schema, migrations for its own tables, store semantics, and conformance tests.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T1
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "64c578e1-e2a1-48d4-8da9-659d4f881ef3"
|
||||
```
|
||||
@@ -64,7 +66,7 @@ schema version reporting.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T2
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "19cfd23e-8a87-416d-b948-c727e8c5a11c"
|
||||
```
|
||||
@@ -76,7 +78,7 @@ security, observability, backup/restore expectations, and acceptance tests.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T3
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d3b388de-bb79-41d5-805e-d2def88ac926"
|
||||
```
|
||||
@@ -88,7 +90,7 @@ secrets, authorization, or audit-platform concerns.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T4
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d0e05af7-d777-4948-b072-79f1ffb9fc3a"
|
||||
```
|
||||
@@ -99,7 +101,7 @@ the isolated MVP without leaking Postgres concepts into domain code.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T5
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "3c428960-be5b-411e-bd9b-7cba833abba8"
|
||||
```
|
||||
@@ -111,7 +113,7 @@ readiness, and redacted diagnostics.
|
||||
|
||||
```task
|
||||
id: USER-WP-0009-T6
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d606094a-254c-46d5-9bb8-a3449ce61c2c"
|
||||
```
|
||||
@@ -133,10 +135,46 @@ expectations, encryption, and operational runbooks.
|
||||
tests.
|
||||
- The provider-repo boundary is explicit and avoids duplicating IAM, secrets,
|
||||
authorization, audit-platform, or infrastructure ownership.
|
||||
- `UserEngineService` depends on an adapter-neutral store protocol with
|
||||
readiness, query, transaction, audit, outbox, and diagnostics semantics.
|
||||
- No Postgres implementation code is added as part of this workplan.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- `docs/postgres-durable-store-consumer-requirements.md`
|
||||
- Store-boundary notes suitable for a future provider repo.
|
||||
- `UserEngineStore` protocol and local-store conformance behavior.
|
||||
- Follow-up implementation workplan inputs for a Postgres adapter.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Implemented on 2026-06-15:
|
||||
|
||||
- Added `UserEngineStore` in `src/user_engine/ports.py` as the durable
|
||||
persistence boundary for service behavior.
|
||||
- Moved `UserEngineService` from the concrete in-memory store type to the
|
||||
store protocol.
|
||||
- Replaced service reads of local dict/list fields with protocol accessors for
|
||||
users, identities, applications, bindings, catalogs, audit, outbox, and
|
||||
diagnostics.
|
||||
- Added store transaction boundaries around mutating writes so domain changes,
|
||||
local audit records, and outbox events commit or roll back together.
|
||||
- Kept authorization-denial audit records durable without emitting outbox
|
||||
events, including when a denial happens inside a composed outer transaction.
|
||||
- Extended `InMemoryUserEngineStore` as the reference adapter with query
|
||||
helpers, record counts, pending outbox access, audit-log access, and nested
|
||||
transaction rollback semantics.
|
||||
- Added conformance tests for protocol-only store consumption, failed-mutation
|
||||
rollback, and denial-audit persistence across rollback.
|
||||
- Updated the durable-store and public contract docs to describe the new
|
||||
adapter boundary.
|
||||
- No Postgres adapter, database dependency, provisioning, credentials, or
|
||||
infrastructure ownership was added.
|
||||
|
||||
Verification:
|
||||
|
||||
```text
|
||||
make test
|
||||
Ran 42 tests in 0.134s
|
||||
OK
|
||||
```
|
||||
|
||||
125
workplans/USER-WP-0010-registration-identity-and-factor-model.md
Normal file
125
workplans/USER-WP-0010-registration-identity-and-factor-model.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
id: USER-WP-0010
|
||||
type: workplan
|
||||
title: "Registration Identity And Factor Model"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 10
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0007
|
||||
- USER-WP-0009
|
||||
state_hub_workstream_id: "0d53560b-2b9d-442b-9328-4b2ce5c5bdae"
|
||||
---
|
||||
|
||||
# USER-WP-0010 - Registration Identity And Factor Model
|
||||
|
||||
## Goal
|
||||
|
||||
Define and implement the first headless registration domain slice for
|
||||
NetKingdom users. The slice should let user-engine start and complete a
|
||||
registration session, establish a stable NetKingdom ID, link verified external
|
||||
identities, record factor evidence, and return identity context without
|
||||
becoming an identity provider or factor-proofing service.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
user-engine owns the registration-domain records and service facade. NetKingdom
|
||||
IAM, identity providers, eID providers, mail/SMS proofing, credential
|
||||
lifecycle, sessions, and tokens remain external adapter concerns.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement password, passkey, session, MFA, SMS, email, or eID proofing
|
||||
providers in user-engine.
|
||||
- Do not issue OIDC/SAML tokens.
|
||||
- Do not build the registration UI in this workplan.
|
||||
- Do not implement prepared account claiming, access profiles, or onboarding
|
||||
journeys beyond the hooks needed for later workplans.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "2a6c93de-e320-41e6-8930-7a4099c5757a"
|
||||
```
|
||||
|
||||
Define NetKingdom ID semantics. Decide whether the public NetKingdom ID is the
|
||||
existing `User.user_id`, an alias, or a separate mapped identifier. Document
|
||||
stability, visibility, privacy, and migration expectations.
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "31ddb44e-b7d1-406e-9114-78c5e7f92478"
|
||||
```
|
||||
|
||||
Add registration session domain models and lifecycle states: started,
|
||||
factor_pending, factor_verified, completed, abandoned, expired, and rejected.
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7441f064-eb49-4e66-8c1d-a2626aae020c"
|
||||
```
|
||||
|
||||
Add identity factor and factor verification models for email, phone, postal
|
||||
address, eID, invite, and SSO identity evidence. Store assurance metadata and
|
||||
evidence references without storing secret proofing payloads.
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T4
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7057afda-d585-48cd-bac1-f0bd0f05fef5"
|
||||
```
|
||||
|
||||
Create factor verification adapter ports. The adapters should accept external
|
||||
proofing results and return normalized factor evidence for user-engine.
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T5
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "f4f0da38-9810-45e7-ab4e-0619eb45b3c4"
|
||||
```
|
||||
|
||||
Implement a headless registration facade for start, attach verified factor,
|
||||
complete, abandon, and resume flows.
|
||||
|
||||
```task
|
||||
id: USER-WP-0010-T6
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "c29b31cd-f2b2-41b6-86ee-9c78470abf01"
|
||||
```
|
||||
|
||||
Add audit, outbox, diagnostics, and redaction behavior for registration and
|
||||
factor lifecycle transitions.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A caller can start and complete a headless registration flow from verified
|
||||
factor evidence.
|
||||
- Completed registration creates or resolves a stable NetKingdom user/account
|
||||
and external identity links.
|
||||
- Factor evidence is inspectable through safe metadata and evidence references,
|
||||
not raw proofing secrets.
|
||||
- Registration failure, expiry, and abandon states are auditable.
|
||||
- No credential, token, or proofing provider ownership moves into user-engine.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Registration and factor domain models.
|
||||
- Registration service facade.
|
||||
- Factor verification adapter ports.
|
||||
- Documentation and tests for the basic self-registration flow.
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
id: USER-WP-0011
|
||||
type: workplan
|
||||
title: "Prepared Accounts And Entitlement Claims"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 11
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0010
|
||||
state_hub_workstream_id: "39ac9f87-c61d-42d8-a45f-bece4848ed47"
|
||||
---
|
||||
|
||||
# USER-WP-0011 - Prepared Accounts And Entitlement Claims
|
||||
|
||||
## Goal
|
||||
|
||||
Allow NetKingdom operators, tenant admins, family owners, service owners, or
|
||||
upstream systems to prepare account intent and access packages before the user
|
||||
registers. When the user later proves matching factors, user-engine can attach
|
||||
the prepared package to the canonical user and activate the right lifecycle
|
||||
steps.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
Prepared accounts are not credentials. They are pending user-domain facts:
|
||||
expected factor matches, tenant or group references, planned memberships,
|
||||
profile defaults, onboarding journey hints, approval gates, expiry, and audit
|
||||
history.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not create login credentials for users who have not registered.
|
||||
- Do not bypass factor verification or approval policies.
|
||||
- Do not make user-engine the source of truth for external organization, HR, or
|
||||
directory records.
|
||||
- Do not implement final authorization policy decisions.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "11508f77-170b-4b22-bfdc-115a69bfe4db"
|
||||
```
|
||||
|
||||
Add prepared account and prepared entitlement models with status, expiry,
|
||||
preparer identity, tenant/scope references, factor match requirements, and
|
||||
audit metadata.
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "86ca36d4-721b-48fe-8c0c-c6a1e6740d2f"
|
||||
```
|
||||
|
||||
Implement create, update, revoke, expire, and list operations for prepared
|
||||
accounts, guarded by the authorization port.
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "fe5a08e8-1101-4cec-b02f-b2eee8928604"
|
||||
```
|
||||
|
||||
Implement claim matching during registration. Match verified factor evidence to
|
||||
prepared account requirements and produce explicit claim decisions.
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T4
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "8aef6d9e-5e76-4e44-bf81-58049b22a25c"
|
||||
```
|
||||
|
||||
Convert claimed prepared entitlements into user-engine-owned facts:
|
||||
memberships, tenant accounts, profile defaults, application bindings, and
|
||||
onboarding journey starts.
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T5
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "527519a1-48ed-45fc-a6fc-739986ae6303"
|
||||
```
|
||||
|
||||
Add conflict and safety rules for duplicate prepared accounts, weak factor
|
||||
matches, expired packages, privileged roles, and manual approval requirements.
|
||||
|
||||
```task
|
||||
id: USER-WP-0011-T6
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "9530c8d6-82af-4635-8af8-aa79c54be94d"
|
||||
```
|
||||
|
||||
Add audit/outbox events and evidence references for preparation, claim,
|
||||
activation, denial, expiry, and revocation.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A prepared account can be created before user registration without issuing
|
||||
credentials.
|
||||
- A registering user can claim prepared rights only when required factor
|
||||
evidence matches.
|
||||
- Claimed rights become explicit user-engine memberships, profile values,
|
||||
tenant account state, and onboarding events.
|
||||
- Expired, revoked, ambiguous, or privileged claims fail closed.
|
||||
- Every preparation and claim decision is auditable.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Prepared account domain model.
|
||||
- Prepared entitlement activation facade.
|
||||
- Claim matching rules and tests.
|
||||
- Documentation for account preparation boundaries.
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
id: USER-WP-0012
|
||||
type: workplan
|
||||
title: "Hats, Realms, Services, Assets, And Access Profiles"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: high
|
||||
planning_order: 12
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0010
|
||||
state_hub_workstream_id: "f3cf0d30-eb6b-4734-a0a3-5a755d4cf150"
|
||||
---
|
||||
|
||||
# USER-WP-0012 - Hats, Realms, Services, Assets, And Access Profiles
|
||||
|
||||
## Goal
|
||||
|
||||
Model how users and groups wear different hats across NetKingdom realms,
|
||||
services, and assets. Provide access-control facts, profile layers, and
|
||||
claims-enrichment context that authorization systems and service runtimes can
|
||||
consume without moving final policy decisions into user-engine.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
user-engine owns the identity-domain representation of hats, memberships,
|
||||
access profiles, and active context. Authorization engines own policy decisions
|
||||
and protected services own runtime enforcement.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement the final ACL enforcement engine.
|
||||
- Do not define every service-specific permission in user-engine.
|
||||
- Do not bypass the authorization port.
|
||||
- Do not make browser/UI state the source of truth for active access context.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "b86f0072-e666-479b-9b90-96d4015bbfa0"
|
||||
```
|
||||
|
||||
Define realm, service area, asset scope, access profile, group, and hat
|
||||
vocabulary. Map each concept to current user-engine membership, profile, and
|
||||
canon reference patterns.
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "66117083-8e85-44e1-9a76-cfd10dd24d23"
|
||||
```
|
||||
|
||||
Add hat selection and active context models. A user should be able to choose an
|
||||
active hat for a tenant, realm, service, or asset context when allowed.
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "1dffda4c-f979-480e-9d6d-12ec9576780d"
|
||||
```
|
||||
|
||||
Implement access profile templates that combine memberships, factor assurance
|
||||
requirements, profile defaults, and claims projection rules.
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T4
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "b07494fe-f301-49e2-8ea8-267a4c5219ee"
|
||||
```
|
||||
|
||||
Extend `identity_context` and claims-enrichment projections with active hat,
|
||||
realm, service, asset, group, access profile, and evidence references.
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T5
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "c78e10c4-b245-4a83-a75d-4b46a6073fd2"
|
||||
```
|
||||
|
||||
Add ports for exporting access-control facts to authorization engines or ACL
|
||||
systems while preserving source-of-truth boundaries.
|
||||
|
||||
```task
|
||||
id: USER-WP-0012-T6
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "f9f32165-3a12-424e-a370-bb2ab8348c21"
|
||||
```
|
||||
|
||||
Add tests for hat selection, cross-tenant denial, missing factor assurance,
|
||||
group-derived access, service-specific projection, and redacted diagnostics.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Users can have multiple hats without collapsing them into one account state.
|
||||
- Active hat context is explicit in identity context and projections.
|
||||
- Access profile facts can be exported to authorization systems.
|
||||
- Missing tenant, realm, service, asset, factor, or approval context fails
|
||||
closed.
|
||||
- Final policy and ACL enforcement remain outside user-engine.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Hat and access profile domain model.
|
||||
- Active context service facade.
|
||||
- Identity-context and claims projection updates.
|
||||
- Access-control fact export tests.
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
id: USER-WP-0013
|
||||
type: workplan
|
||||
title: "Onboarding Journeys And Welcome Protocols"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: medium
|
||||
planning_order: 13
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0011
|
||||
- USER-WP-0012
|
||||
state_hub_workstream_id: "1dc82dfd-be68-4585-b6c9-6d24aebd3e27"
|
||||
---
|
||||
|
||||
# USER-WP-0013 - Onboarding Journeys And Welcome Protocols
|
||||
|
||||
## Goal
|
||||
|
||||
Create a journey layer that helps newly registered or newly entitled users
|
||||
enter the right NetKingdom subsystems. Welcome protocols should be driven by
|
||||
registration, prepared-account, invitation, role, profile, and access events.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
user-engine owns journey state, task references, event correlation, and user
|
||||
context. Delivery systems, protected services, help content, notification
|
||||
channels, and external task systems remain adapters or downstream systems.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not build a notification platform.
|
||||
- Do not embed service-specific tours or support content in core domain code.
|
||||
- Do not replace external workflow/task systems.
|
||||
- Do not build the UI in this workplan.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0013-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "30ef8507-eebc-4b96-8aa6-c530bef05739"
|
||||
```
|
||||
|
||||
Define onboarding journey, welcome protocol, journey step, task, and subsystem
|
||||
handoff models.
|
||||
|
||||
```task
|
||||
id: USER-WP-0013-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "7c6e53d4-ff96-4036-a413-f04b4b73d266"
|
||||
```
|
||||
|
||||
Add journey templates keyed by registration outcome, prepared entitlement,
|
||||
tenant, realm, service, application, role, hat, and factor requirements.
|
||||
|
||||
```task
|
||||
id: USER-WP-0013-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "d9c2983a-45d1-4b1b-a416-63e180ca74b3"
|
||||
```
|
||||
|
||||
Implement journey start, progress, complete, skip, fail, and resume operations
|
||||
with authorization, audit, and outbox behavior.
|
||||
|
||||
```task
|
||||
id: USER-WP-0013-T4
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "7155c2eb-4e32-46f0-ad33-961784cb9a03"
|
||||
```
|
||||
|
||||
Add adapter ports for notifications, task systems, support content, subsystem
|
||||
welcome callbacks, and lifecycle task linking.
|
||||
|
||||
```task
|
||||
id: USER-WP-0013-T5
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "c5e42dd6-207a-4b1e-a0d8-35701e9f71bc"
|
||||
```
|
||||
|
||||
Expose onboarding status through identity context, diagnostics, and optional UI
|
||||
contracts.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Registration or prepared-account claim can start an onboarding journey.
|
||||
- Journey state is resumable, auditable, and correlated with outbox events.
|
||||
- Subsystem welcome steps are adapter-driven, not hard-coded into core
|
||||
registration logic.
|
||||
- Users and admins can inspect pending onboarding work and blocked steps.
|
||||
- Missing subsystem callbacks produce explicit lifecycle gaps.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Onboarding journey domain model.
|
||||
- Welcome protocol service facade.
|
||||
- Adapter ports for notifications and subsystem handoff.
|
||||
- Scenario tests for successful, blocked, and resumed onboarding.
|
||||
125
workplans/USER-WP-0014-registration-and-access-management-ui.md
Normal file
125
workplans/USER-WP-0014-registration-and-access-management-ui.md
Normal file
@@ -0,0 +1,125 @@
|
||||
---
|
||||
id: USER-WP-0014
|
||||
type: workplan
|
||||
title: "Registration And Access Management UI"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: medium
|
||||
planning_order: 14
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0010
|
||||
- USER-WP-0011
|
||||
- USER-WP-0012
|
||||
- USER-WP-0013
|
||||
state_hub_workstream_id: "011f7d20-5c9d-42a9-b7a3-b20a8ae9f557"
|
||||
---
|
||||
|
||||
# USER-WP-0014 - Registration And Access Management UI
|
||||
|
||||
## Goal
|
||||
|
||||
Build an optional NetKingdom registration and access management UI backed by
|
||||
user-engine APIs. The UI should make registration, factor status, prepared
|
||||
rights, hat selection, profile completion, and onboarding journeys convenient
|
||||
without hiding IAM, authorization, proofing, or service-runtime boundaries.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
The UI is an operating surface over user-engine domain APIs. It should be thin,
|
||||
workflow-oriented, and suitable for self-service users, tenant admins, family
|
||||
owners, and operators.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not implement credential entry, password reset, passkeys, MFA challenges,
|
||||
or token issuance in the UI.
|
||||
- Do not embed final authorization policy rules in frontend code.
|
||||
- Do not replace service-specific admin consoles.
|
||||
- Do not make UI state authoritative over domain records.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "983087e1-c512-419f-86a6-b954d0a1ab54"
|
||||
```
|
||||
|
||||
Define UI information architecture for registration, factor status,
|
||||
prepared-account claim, hat selection, profile completion, onboarding journey,
|
||||
and admin setup views.
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "0af5d8ef-0d1e-44bd-b807-bc40e87afef2"
|
||||
```
|
||||
|
||||
Define UI API contracts or route handlers over the headless service facades.
|
||||
Keep proofing, IAM, authorization, and notification calls behind adapters.
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "a2e00aa3-5849-469c-a3a3-f4f5bd2df6c8"
|
||||
```
|
||||
|
||||
Implement the self-service registration flow with resume, prepared rights
|
||||
review, factor status, terms/consent, and completion states.
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T4
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "36d49049-cfe7-4f87-9a7f-78e37de9188a"
|
||||
```
|
||||
|
||||
Implement hat selection and active access context views for realms, services,
|
||||
groups, and assets.
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T5
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "e58038fc-6138-40cc-bb6b-4cbf7a8b0b87"
|
||||
```
|
||||
|
||||
Implement admin views for prepared accounts, invitations, access profiles,
|
||||
group membership, realms/services/assets, and onboarding diagnostics.
|
||||
|
||||
```task
|
||||
id: USER-WP-0014-T6
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "4de949d6-e330-41b2-87cf-9b9425f0f8be"
|
||||
```
|
||||
|
||||
Add usability, accessibility, error-state, redaction, and mobile/desktop tests
|
||||
for the registration and admin flows.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A new user can complete a registration flow through the UI using adapter
|
||||
supplied factor evidence.
|
||||
- A prepared account claim can be reviewed and accepted or denied through the
|
||||
UI.
|
||||
- Users can choose an active hat and see available realms/services without
|
||||
exposing internal policy logic.
|
||||
- Admins can prepare accounts and inspect onboarding state.
|
||||
- The UI does not store or display secrets, raw proofing payloads, or hidden
|
||||
authorization decisions.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Registration UI and API contract.
|
||||
- Hat/access management UI views.
|
||||
- Admin prepared-account and onboarding views.
|
||||
- Frontend verification artifacts.
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
id: USER-WP-0015
|
||||
type: workplan
|
||||
title: "Registration Scenario And Security Conformance"
|
||||
domain: netkingdom
|
||||
repo: user-engine
|
||||
status: proposed
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
planning_priority: medium
|
||||
planning_order: 15
|
||||
created: "2026-06-15"
|
||||
updated: "2026-06-15"
|
||||
depends_on:
|
||||
- USER-WP-0010
|
||||
- USER-WP-0011
|
||||
- USER-WP-0012
|
||||
- USER-WP-0013
|
||||
- USER-WP-0014
|
||||
state_hub_workstream_id: "4f21e1c9-ad27-4ac9-888f-8f78c6abfb3b"
|
||||
---
|
||||
|
||||
# USER-WP-0015 - Registration Scenario And Security Conformance
|
||||
|
||||
## Goal
|
||||
|
||||
Prove the full NetKingdom registration and onboarding model through executable
|
||||
scenarios, security negative paths, redaction checks, adapter conformance, and
|
||||
operability diagnostics.
|
||||
|
||||
## Scope Direction
|
||||
|
||||
This workplan turns the registration roadmap into a testable contract. It
|
||||
should cover both headless APIs and the optional UI surface where present.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Do not add new product surface unless a test exposes a missing contract.
|
||||
- Do not assert provider-specific IAM, eID, SMS, email, or authorization engine
|
||||
internals.
|
||||
- Do not require production infrastructure for local conformance tests.
|
||||
|
||||
## Tasks
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T1
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "5ca0a269-559d-4138-b702-9984a411f2ed"
|
||||
```
|
||||
|
||||
Define the registration scenario matrix: self-registration, prepared account
|
||||
claim, privileged role requiring approval, eID-backed assurance, family invite,
|
||||
tenant admin invite, group access, and denied cross-tenant claim.
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T2
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "6ee492b1-923f-4aa0-8e17-b69f522c4898"
|
||||
```
|
||||
|
||||
Add end-to-end headless tests covering registration through identity context,
|
||||
claims enrichment, active hat selection, and onboarding event emission.
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T3
|
||||
status: todo
|
||||
priority: high
|
||||
state_hub_task_id: "b813a88f-ced6-40ce-9a25-d1c666fb73c9"
|
||||
```
|
||||
|
||||
Add security negative tests for weak factor evidence, duplicate identity links,
|
||||
prepared-account hijack attempts, expired claims, missing tenant context,
|
||||
privileged role escalation, and stale approvals.
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T4
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "5a03ac1a-1f8e-455b-8f75-691e8bdda286"
|
||||
```
|
||||
|
||||
Add redaction and diagnostics tests for factor values, profile sensitivity,
|
||||
prepared-account metadata, active hat context, and access-profile evidence.
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T5
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "fcf32b4d-d050-4989-bb05-844e0d13e548"
|
||||
```
|
||||
|
||||
Add adapter conformance tests for factor verification, authorization checks,
|
||||
access fact export, onboarding handoff, audit export, outbox replay, and
|
||||
durable store behavior.
|
||||
|
||||
```task
|
||||
id: USER-WP-0015-T6
|
||||
status: todo
|
||||
priority: medium
|
||||
state_hub_task_id: "a7850784-3b86-453f-bbc7-1d53d0813f82"
|
||||
```
|
||||
|
||||
Add UI flow tests once USER-WP-0014 exists: registration happy path, resume,
|
||||
prepared rights review, hat selection, admin preparation, and blocked journey.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- The main registration and onboarding journeys are executable as tests.
|
||||
- Security negative paths fail closed and leave audit evidence.
|
||||
- Sensitive factor and profile data is redacted from diagnostics and UI output.
|
||||
- Adapter contracts are testable without production infrastructure.
|
||||
- The registration UI, if implemented, is covered by workflow-level tests.
|
||||
|
||||
## Expected Outputs
|
||||
|
||||
- Registration scenario matrix.
|
||||
- Headless and UI conformance tests.
|
||||
- Security negative-path test suite.
|
||||
- Adapter conformance harness for registration dependencies.
|
||||
Reference in New Issue
Block a user