test: add registration security conformance

This commit is contained in:
2026-06-15 23:59:45 +02:00
parent aaefa48212
commit 2ceecf6463
10 changed files with 846 additions and 11 deletions

View File

@@ -16,6 +16,7 @@ See `docs/development.md`, `docs/configuration.md`, `docs/contracts.md`,
`docs/hats-realms-services-assets-access-profiles.md`,
`docs/onboarding-journeys-and-welcome-protocols.md`,
`docs/registration-and-access-management-ui.md`, `docs/scenarios.md`,
`docs/registration-scenario-and-security-conformance.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

View File

@@ -64,5 +64,5 @@ accounts and entitlement claims. `USER-WP-0012` implements hats, realms,
services, assets, access profiles, active context, and exportable
access-control facts. `USER-WP-0013` implements onboarding journeys and
welcome protocols. `USER-WP-0014` implements the optional registration and
access-management UI contract facade. `USER-WP-0015` remains proposed future
work for security conformance.
access-management UI contract facade. `USER-WP-0015` implements registration
scenario and security conformance tests.

View File

@@ -43,6 +43,19 @@ accessible HTML verification. It does not handle credential entry, MFA
challenges, token issuance, hidden policy decisions, notifications, or
service-specific admin consoles.
## Scenario And Security Conformance Contract
`user_engine.testing.scenarios` defines `SCENARIO_MATRIX` and
`REGISTRATION_SCENARIO_MATRIX` for local conformance. The matrix covers
self-registration, prepared-account claims, privileged approval gates,
eID-backed assurance, family invite, tenant admin invite, group access,
cross-tenant denial, and USER-WP-0014 UI workflows.
Conformance tests must run without production IAM, proofing, notification,
workflow, authorization-engine, or database infrastructure. They exercise
adapter seams with local harnesses and assert fail-closed behavior, audit
evidence, outbox replay, redaction, and durable transaction semantics.
## Registration Contract
Registration is a headless user-entry facade. It creates a

View File

@@ -236,8 +236,8 @@ once.
## Recommended Workplans
As of 2026-06-15, `USER-WP-0010`, `USER-WP-0011`, `USER-WP-0012`,
`USER-WP-0013`, and `USER-WP-0014` are implemented as user-engine slices. The
later security-conformance workplan remains recommended follow-on work.
`USER-WP-0013`, `USER-WP-0014`, and `USER-WP-0015` are implemented as
user-engine slices.
| Workplan | Title | Purpose |
| --- | --- | --- |

View File

@@ -0,0 +1,108 @@
# Registration Scenario And Security Conformance
Status: implemented conformance slice
Date: 2026-06-15
Related workplan: USER-WP-0015
## Purpose
This slice turns the NetKingdom registration and onboarding roadmap into an
executable local conformance contract. It proves that the headless APIs and the
optional UI contract can complete the main registration journey, fail closed on
security negative paths, redact sensitive values, and exercise adapter seams
without production infrastructure.
## Scenario Matrix
The registration matrix is defined in `user_engine.testing.scenarios` as
`REGISTRATION_SCENARIO_MATRIX`.
It covers:
- self-registration;
- prepared account claim;
- privileged role requiring approval;
- eID-backed assurance;
- family invite;
- tenant admin invite;
- group access;
- denied cross-tenant claim.
The broader `SCENARIO_MATRIX` now also names registration/onboarding,
prepared-claim, group-access hat, denied cross-tenant claim, and UI workflow
coverage.
## End-To-End Conformance
`tests/test_registration_security_conformance.py` includes a full local flow:
```text
register application/catalog
prepare account with onboarding hint
complete email-backed registration
claim prepared account
select active hat
read claims projection and identity context
export access-control facts
render admin UI
assert onboarding event emission and redaction
```
This proves the path from registration through identity context, claims
enrichment, active access context, access fact export, onboarding, and UI
diagnostics.
## Security Negative Paths
The conformance suite exercises fail-closed behavior for:
- weak factor requirements;
- duplicate identity links;
- prepared-account hijack attempts;
- expired prepared claims;
- missing or cross-tenant context;
- privileged prepared roles requiring approval;
- stale approval through approval-required access profiles.
Denied prepared-account claim decisions leave audit evidence without creating
memberships for the attacker or emitting successful activation events.
## Redaction And Diagnostics
Conformance checks assert that these values do not leak through diagnostics,
events, or UI output:
- normalized factor values;
- email addresses used for prepared account matching;
- sensitive profile values;
- access profile claim/default values;
- proofing adapter secret input.
Sensitive profile values are redacted in runtime projections as
`<redacted>`.
## Adapter Conformance
The local adapter conformance path covers:
- factor verification adapter normalization;
- authorization harness request capture and obligations;
- access-control fact export;
- onboarding handoff lifecycle gaps and resume;
- audit record availability;
- outbox replay through pending events;
- in-memory durable-store rollback behavior in existing port tests.
No provider-specific IAM, eID, SMS, email, authorization-engine, notification,
or workflow infrastructure is required.
## Commands
```bash
make test
make test-conformance
```
The Makefile targets currently run the same standard-library test suite. They
remain separate entry points so CI can split unit, integration, scenario, and
conformance execution later.

View File

@@ -16,6 +16,27 @@ projection, audit, and event behavior testable without a UI.
| audit_event_replay | Mutations carry audit records, outbox events, and correlation ids. |
| identity_canon_context | Actor, user, account, authenticated subject, authorization principal, tenant, membership, grant-like facts, and evidence references stay distinguishable. |
| family_dataspace_onboarding | A family tenant can register a personal dataspace, invite members, accept SSO identities, project claims context, and deny cross-family access. |
| registration_onboarding_full | Registration, prepared claim, active hat, claims projection, onboarding, access fact export, and UI diagnostics work as one local flow. |
| prepared_account_claim | Prepared rights can be claimed only after matching verified factors. |
| privileged_role_requires_approval | Privileged prepared roles fail closed without approval. |
| eid_assurance_registration | eID-backed factor evidence can participate in registration conformance. |
| tenant_admin_invite | Tenant admins can prepare users and inspect diagnostics without issuing credentials. |
| group_access_hat | Group-derived memberships can produce active hat and access-control facts. |
| denied_cross_tenant_claim | Cross-tenant prepared claims and tenant overreach fail closed. |
| ui_registration_access_flow | USER-WP-0014 UI contracts cover registration, prepared rights, hats, admin diagnostics, redaction, and responsive metadata. |
## Registration Scenario Matrix
`REGISTRATION_SCENARIO_MATRIX` covers:
- self-registration;
- prepared account claim;
- privileged role requiring approval;
- eID-backed assurance;
- family invite;
- tenant admin invite;
- group access;
- denied cross-tenant claim.
## Fixture Actors

View File

@@ -26,7 +26,67 @@ SCENARIO_MATRIX = (
"two_applications",
"sensitive_redaction",
"audit_event_replay",
"identity_canon_context",
"family_dataspace_onboarding",
"registration_onboarding_full",
"prepared_account_claim",
"privileged_role_requires_approval",
"eid_assurance_registration",
"tenant_admin_invite",
"group_access_hat",
"denied_cross_tenant_claim",
"ui_registration_access_flow",
)
REGISTRATION_SCENARIO_MATRIX = (
{
"id": "self_registration",
"actor": "human",
"factors": ("email",),
"expects": ("registration.completed", "identity_context", "netkingdom_id"),
},
{
"id": "prepared_account_claim",
"actor": "human",
"factors": ("email",),
"expects": ("prepared_account.claimed", "membership", "onboarding_journey"),
},
{
"id": "privileged_role_requires_approval",
"actor": "human",
"factors": ("email",),
"expects": ("authorization_denied", "no_membership_mutation"),
},
{
"id": "eid_assurance_registration",
"actor": "human",
"factors": ("eid",),
"expects": ("registration.completed", "high_assurance_factor"),
},
{
"id": "family_invite",
"actor": "family-owner",
"factors": ("sso",),
"expects": ("family_invitation.accepted", "claims_projection"),
},
{
"id": "tenant_admin_invite",
"actor": "tenant-admin",
"factors": ("email",),
"expects": ("prepared_account.created", "tenant_diagnostics"),
},
{
"id": "group_access",
"actor": "human",
"factors": ("email",),
"expects": ("active_access_context", "access_control_fact"),
},
{
"id": "denied_cross_tenant_claim",
"actor": "human",
"factors": ("email",),
"expects": ("authorization_denied", "audit_record", "no_outbox_event"),
},
)

View File

@@ -18,6 +18,7 @@ from user_engine.projections import ClaimsEnrichmentProjectionCache
from user_engine.service import REDACTED, UserEngineService
from user_engine.testing.fixtures import sample_application, sample_application_binding
from user_engine.testing.scenarios import (
REGISTRATION_SCENARIO_MATRIX,
SCENARIO_MATRIX,
ScenarioAuthorizationHarness,
StrictFixtureIdentityClaimsAdapter,
@@ -44,7 +45,29 @@ class IntegratedScenarioTests(unittest.TestCase):
"two_applications",
"sensitive_redaction",
"audit_event_replay",
"identity_canon_context",
"family_dataspace_onboarding",
"registration_onboarding_full",
"prepared_account_claim",
"privileged_role_requires_approval",
"eid_assurance_registration",
"tenant_admin_invite",
"group_access_hat",
"denied_cross_tenant_claim",
"ui_registration_access_flow",
},
)
self.assertEqual(
{scenario["id"] for scenario in REGISTRATION_SCENARIO_MATRIX},
{
"self_registration",
"prepared_account_claim",
"privileged_role_requires_approval",
"eid_assurance_registration",
"family_invite",
"tenant_admin_invite",
"group_access",
"denied_cross_tenant_claim",
},
)

View File

@@ -0,0 +1,576 @@
from dataclasses import replace
from datetime import timedelta
import unittest
from user_engine.adapters.local import InMemoryUserEngineStore, LocalAuthorizationCheckPort
from user_engine.domain import (
AccessMembershipRequirement,
AccessProfile,
AccessScopeType,
AccountStatus,
AttributeDefinition,
Catalog,
CatalogLifecycle,
FactorVerification,
IdentityFactorType,
Mutability,
OnboardingJourneyStatus,
OnboardingTriggerType,
PreparedEntitlement,
PreparedEntitlementKind,
PreparedFactorRequirement,
ProfileScope,
ProjectionType,
Sensitivity,
Visibility,
WelcomeProtocol,
WelcomeProtocolStep,
utc_now,
)
from user_engine.errors import AuthorizationDenied, ConflictError, ValidationError
from user_engine.service import REDACTED, UserEngineService
from user_engine.testing.fixtures import (
FixtureIdentityClaimsAdapter,
human_actor_claims,
sample_application,
sample_application_binding,
)
from user_engine.testing.scenarios import (
ScenarioAuthorizationHarness,
StrictFixtureIdentityClaimsAdapter,
missing_tenant_claims,
)
from user_engine.ui import RegistrationAccessManagementUi, UiViewport
class RegistrationSecurityConformanceTests(unittest.TestCase):
def test_full_registration_claim_hat_onboarding_ui_conformance_path(self):
service, store, _ = _service()
actor = _actor("conformance-user")
_bootstrap_application(service, actor)
service.register_welcome_protocol(
actor,
_prepared_welcome_protocol(),
correlation_id="corr-conf-protocol",
)
prepared = service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(_email_requirement(),),
entitlements=(
PreparedEntitlement(
kind=PreparedEntitlementKind.MEMBERSHIP,
tenant="tenant:coulomb",
scope_type="realm",
scope_id="realm:citadel",
role="operator",
),
PreparedEntitlement(
kind=PreparedEntitlementKind.ONBOARDING_JOURNEY,
tenant="tenant:coulomb",
onboarding_journey="welcome-demo",
),
),
correlation_id="corr-conf-prepare",
)
registration = _complete_registration(service, actor)
claim = service.claim_prepared_account(
actor,
registration.session.registration_id,
prepared_account_id=prepared.prepared_account_id,
correlation_id="corr-conf-claim",
)
profile = service.register_access_profile(
actor,
_operator_access_profile(),
correlation_id="corr-conf-profile",
)
selection = service.select_active_hat(
actor,
registration.user.user_id,
profile.access_profile_id,
correlation_id="corr-conf-hat",
)
projection = service.projection(
actor,
registration.user.user_id,
ProjectionType.CLAIMS_ENRICHMENT,
application_id="app.demo",
tenant="tenant:coulomb",
correlation_id="corr-conf-projection",
)
context = service.identity_context(
actor,
user_id=registration.user.user_id,
tenant="tenant:coulomb",
application_id="app.demo",
correlation_id="corr-conf-context",
)
export = service.export_access_control_facts(
actor,
tenant="tenant:coulomb",
user_id=registration.user.user_id,
correlation_id="corr-conf-export",
)
ui_html = RegistrationAccessManagementUi(service).render_html(
RegistrationAccessManagementUi(service).admin_dashboard(
actor,
tenant="tenant:coulomb",
viewport=UiViewport.DESKTOP,
)
)
self.assertEqual(claim.memberships[0].kind, "operator")
self.assertEqual(selection.active_context.hat, "operator")
self.assertEqual(projection.access_context["active_hat"], "operator")
self.assertTrue(context.onboarding_journeys)
self.assertEqual(
context.onboarding_journeys[0].status,
OnboardingJourneyStatus.IN_PROGRESS,
)
self.assertIn("user", export.manifest["subject_types"])
self.assertIn(
"onboarding_journey.started",
[event.event_type for event in service.outbox_events()],
)
self.assertNotIn("sample.user@example.test", ui_html)
self.assertEqual(store.record_counts()["onboarding_journeys"], 1)
def test_security_negative_paths_fail_closed_with_audit_evidence(self):
service, store, _ = _service()
actor = _actor("security-user")
_bootstrap_application(service, actor)
registration = _complete_registration(service, actor)
other_user = service.create_user(
actor,
display_name="Other",
primary_email="other@example.test",
correlation_id="corr-other",
)
with self.assertRaises(ConflictError):
service.link_identity(
actor,
other_user.user_id,
issuer=actor.issuer,
subject=actor.subject,
correlation_id="corr-duplicate-identity",
)
with self.assertRaises(ValidationError):
service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(
PreparedFactorRequirement(
factor_type=IdentityFactorType.EMAIL,
normalized_value=" ",
),
),
entitlements=(_membership_entitlement(),),
correlation_id="corr-weak-factor",
)
hijack = service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(
PreparedFactorRequirement(
factor_type=IdentityFactorType.EMAIL,
normalized_value="victim@example.test",
),
),
entitlements=(_membership_entitlement(),),
correlation_id="corr-hijack-prepare",
)
with self.assertRaises(ValidationError):
service.claim_prepared_account(
actor,
registration.session.registration_id,
prepared_account_id=hijack.prepared_account_id,
correlation_id="corr-hijack-claim",
)
expired = service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(_email_requirement("expired@example.test"),),
entitlements=(_membership_entitlement(scope_id="realm:expired"),),
expires_at=utc_now() - timedelta(days=1),
correlation_id="corr-expired-prepare",
)
with self.assertRaises(ValidationError):
service.claim_prepared_account(
actor,
registration.session.registration_id,
prepared_account_id=expired.prepared_account_id,
correlation_id="corr-expired-claim",
)
with self.assertRaises(AuthorizationDenied):
service.resolve_tenant_context(actor, "tenant:faraday")
privileged = service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(_email_requirement(),),
entitlements=(
replace(
_membership_entitlement(scope_id="realm:privileged"),
role="admin",
requires_approval=True,
),
),
correlation_id="corr-privileged-prepare",
)
with self.assertRaises(AuthorizationDenied):
service.claim_prepared_account(
actor,
registration.session.registration_id,
prepared_account_id=privileged.prepared_account_id,
correlation_id="corr-privileged-claim",
)
access_profile = service.register_access_profile(
actor,
replace(_operator_access_profile(), requires_approval=True),
correlation_id="corr-stale-approval-profile",
)
with self.assertRaises(AuthorizationDenied):
service.select_active_hat(
actor,
registration.user.user_id,
access_profile.access_profile_id,
correlation_id="corr-stale-approval",
)
audit_summaries = [record.summary for record in service.audit_records()]
self.assertIn(
"prepared account claim denied: factor mismatch or closed",
audit_summaries,
)
self.assertIn(
"prepared account claim denied: approval required",
audit_summaries,
)
self.assertEqual(store.memberships_for_user(other_user.user_id), ())
def test_redaction_and_diagnostics_conformance(self):
service, _, _ = _service()
actor = _actor("redaction-user")
_bootstrap_application(service, actor, catalog=_sensitive_catalog())
registration = _complete_registration(service, actor)
service.set_profile_value(
actor,
registration.user.user_id,
"demo.recovery_hint",
"blue envelope",
tenant="tenant:coulomb",
correlation_id="corr-sensitive-profile",
)
service.prepare_account(
actor,
tenant="tenant:coulomb",
required_factor_matches=(_email_requirement(),),
entitlements=(_membership_entitlement(),),
primary_email="sample.user@example.test",
correlation_id="corr-redaction-prepare",
)
service.register_access_profile(
actor,
replace(
_operator_access_profile(),
claims={"policy_secret": "do-not-render"},
profile_defaults={"landing_hint": "do-not-render"},
),
correlation_id="corr-redaction-profile",
)
projection = service.projection(
actor,
registration.user.user_id,
ProjectionType.APPLICATION_RUNTIME,
application_id="app.demo",
tenant="tenant:coulomb",
correlation_id="corr-sensitive-projection",
)
access_diagnostics = service.access_profile_diagnostics(
actor,
tenant="tenant:coulomb",
correlation_id="corr-access-diagnostics",
)
admin_html = RegistrationAccessManagementUi(service).render_html(
RegistrationAccessManagementUi(service).admin_dashboard(
actor,
tenant="tenant:coulomb",
viewport=UiViewport.DESKTOP,
)
)
event_payloads = repr([event.payload for event in service.outbox_events()])
self.assertEqual(projection.values["demo.recovery_hint"], REDACTED)
self.assertNotIn("blue envelope", repr(access_diagnostics))
self.assertNotIn("do-not-render", repr(access_diagnostics))
self.assertNotIn("sample.user@example.test", admin_html)
self.assertNotIn("sample.user@example.test", event_payloads)
self.assertNotIn("blue envelope", event_payloads)
def test_adapter_conformance_harnesses_without_production_infrastructure(self):
store = InMemoryUserEngineStore()
authz = ScenarioAuthorizationHarness(
action_obligations={"access_control_facts.export": ("acl:sync",)}
)
service = UserEngineService(
store=store,
identity_adapter=StrictFixtureIdentityClaimsAdapter(),
authorization=authz,
factor_verifier=_FixtureFactorVerifier(),
)
actor = service.identity_adapter.normalize(human_actor_claims())
_bootstrap_application(service, actor)
registration = service.start_registration(actor, correlation_id="corr-adapter-start")
service.attach_registration_factor(
actor,
registration.registration_id,
{"type": "eid", "value": "EID-123", "secret": "strip-me"},
correlation_id="corr-adapter-factor",
)
service.attach_registration_factor(
actor,
registration.registration_id,
{"type": "email", "value": "sample.user@example.test"},
correlation_id="corr-adapter-email",
)
completed = service.complete_registration(
actor,
registration.registration_id,
correlation_id="corr-adapter-complete",
)
service.add_membership(
actor,
completed.user.user_id,
tenant="tenant:coulomb",
scope_type="group",
scope_id="group:research",
kind="member",
correlation_id="corr-adapter-group",
)
export = service.export_access_control_facts(
actor,
tenant="tenant:coulomb",
user_id=completed.user.user_id,
correlation_id="corr-adapter-export",
)
protocol = service.register_welcome_protocol(
actor,
WelcomeProtocol(
tenant="tenant:coulomb",
name="Adapter Handoff",
trigger_type=OnboardingTriggerType.MANUAL,
steps=(
WelcomeProtocolStep(
step_key="callback",
title="Callback",
subsystem="crm",
requires_subsystem_callback=True,
),
),
),
correlation_id="corr-adapter-protocol",
)
blocked = service.start_onboarding_journey(
actor,
completed.user.user_id,
protocol.protocol_id,
correlation_id="corr-adapter-onboarding",
).journey
resumed = service.resume_onboarding_journey(
actor,
blocked.journey_id,
callback_refs={"callback": "crm://welcome/callback"},
correlation_id="corr-adapter-resume",
)
self.assertIn("group", export.manifest["subject_types"])
self.assertTrue(any(request.action == "access_control_facts.export" for request in authz.requests))
self.assertNotIn("strip-me", repr(store.factors_for_user(completed.user.user_id)))
self.assertEqual(blocked.status, OnboardingJourneyStatus.BLOCKED)
self.assertEqual(resumed.status, OnboardingJourneyStatus.IN_PROGRESS)
self.assertTrue(service.audit_records())
self.assertTrue(service.outbox_events())
self.assertEqual(service.operability_snapshot().issues, ())
with self.assertRaises(ValidationError):
service.identity_adapter.normalize(missing_tenant_claims())
class _FixtureFactorVerifier:
def normalize(self, proofing_result):
factor_type = IdentityFactorType(str(proofing_result["type"]))
return FactorVerification(
factor_type=factor_type,
normalized_value=str(proofing_result["value"]).casefold(),
display_value=None,
source_system="fixture-proofing",
assurance={"level": "ial2" if factor_type == IdentityFactorType.EID else "ial1"},
)
def _service():
store = InMemoryUserEngineStore()
service = UserEngineService(
store=store,
identity_adapter=FixtureIdentityClaimsAdapter(),
authorization=LocalAuthorizationCheckPort(),
)
return service, store, service.authorization
def _actor(subject: str):
claims = human_actor_claims(subject=subject, tenant="tenant:coulomb")
claims["roles"] = ["tenant-admin"]
return FixtureIdentityClaimsAdapter().normalize(claims)
def _bootstrap_application(
service: UserEngineService,
actor,
*,
catalog: Catalog | None = None,
) -> None:
service.register_application(
actor,
sample_application(),
binding=sample_application_binding(),
correlation_id="corr-bootstrap-app",
)
service.publish_catalog(
actor,
catalog or _simple_catalog(),
correlation_id="corr-bootstrap-catalog",
)
def _complete_registration(service: UserEngineService, actor):
session = service.start_registration(actor, correlation_id="corr-reg-start")
service.attach_registration_factor(
actor,
session.registration_id,
FactorVerification(
factor_type=IdentityFactorType.EMAIL,
normalized_value="sample.user@example.test",
display_value="sample.user@example.test",
source_system="fixture-email",
),
correlation_id="corr-reg-factor",
)
return service.complete_registration(
actor,
session.registration_id,
correlation_id="corr-reg-complete",
)
def _email_requirement(email: str = "sample.user@example.test"):
return PreparedFactorRequirement(
factor_type=IdentityFactorType.EMAIL,
normalized_value=email,
)
def _membership_entitlement(scope_id: str = "realm:citadel"):
return PreparedEntitlement(
kind=PreparedEntitlementKind.MEMBERSHIP,
tenant="tenant:coulomb",
scope_type="realm",
scope_id=scope_id,
role="operator",
)
def _operator_access_profile() -> AccessProfile:
return AccessProfile(
tenant="tenant:coulomb",
display_name="Operator",
hat="operator",
scope_type=AccessScopeType.REALM,
scope_id="realm:citadel",
realm_id="realm:citadel",
service_id="app.demo",
membership_requirements=(
AccessMembershipRequirement(
scope_type="realm",
scope_id="realm:citadel",
kind="operator",
),
),
required_factor_types=(IdentityFactorType.EMAIL,),
claims={"service_role": "operator"},
)
def _prepared_welcome_protocol() -> WelcomeProtocol:
return WelcomeProtocol(
tenant="tenant:coulomb",
name="Prepared Welcome",
trigger_type=OnboardingTriggerType.PREPARED_ACCOUNT,
journey_key="welcome-demo",
prepared_journey="welcome-demo",
steps=(
WelcomeProtocolStep(
step_key="intro",
title="Intro",
subsystem="portal",
callback_ref="portal://welcome",
requires_subsystem_callback=True,
),
),
)
def _simple_catalog() -> Catalog:
return Catalog(
catalog_id="demo-profile",
namespace="demo",
version="0.1.0",
owning_application_id="app.demo",
lifecycle=CatalogLifecycle.ACTIVE,
attributes=(
AttributeDefinition(
key="demo.display_density",
value_type="string",
scope=ProfileScope.APPLICATION,
sensitivity=Sensitivity.INTERNAL,
visibility=(Visibility.USER, Visibility.APPLICATION),
mutability=(Mutability.USER,),
default="comfortable",
validation={"enum": ["compact", "comfortable"]},
),
),
)
def _sensitive_catalog() -> Catalog:
catalog = _simple_catalog()
return Catalog(
catalog_id=catalog.catalog_id,
namespace=catalog.namespace,
version=catalog.version,
owning_application_id=catalog.owning_application_id,
lifecycle=catalog.lifecycle,
attributes=(
*catalog.attributes,
AttributeDefinition(
key="demo.recovery_hint",
value_type="string",
scope=ProfileScope.GLOBAL,
sensitivity=Sensitivity.SENSITIVE,
visibility=(Visibility.USER, Visibility.APPLICATION, Visibility.ADMIN),
mutability=(Mutability.USER, Mutability.ADMIN),
),
),
)
if __name__ == "__main__":
unittest.main()

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Registration Scenario And Security Conformance"
domain: netkingdom
repo: user-engine
status: proposed
status: finished
owner: codex
topic_slug: netkingdom
planning_priority: medium
@@ -44,7 +44,7 @@ should cover both headless APIs and the optional UI surface where present.
```task
id: USER-WP-0015-T1
status: todo
status: done
priority: high
state_hub_task_id: "5ca0a269-559d-4138-b702-9984a411f2ed"
```
@@ -55,7 +55,7 @@ tenant admin invite, group access, and denied cross-tenant claim.
```task
id: USER-WP-0015-T2
status: todo
status: done
priority: high
state_hub_task_id: "6ee492b1-923f-4aa0-8e17-b69f522c4898"
```
@@ -65,7 +65,7 @@ claims enrichment, active hat selection, and onboarding event emission.
```task
id: USER-WP-0015-T3
status: todo
status: done
priority: high
state_hub_task_id: "b813a88f-ced6-40ce-9a25-d1c666fb73c9"
```
@@ -76,7 +76,7 @@ privileged role escalation, and stale approvals.
```task
id: USER-WP-0015-T4
status: todo
status: done
priority: medium
state_hub_task_id: "5a03ac1a-1f8e-455b-8f75-691e8bdda286"
```
@@ -86,7 +86,7 @@ prepared-account metadata, active hat context, and access-profile evidence.
```task
id: USER-WP-0015-T5
status: todo
status: done
priority: medium
state_hub_task_id: "fcf32b4d-d050-4989-bb05-844e0d13e548"
```
@@ -97,7 +97,7 @@ durable store behavior.
```task
id: USER-WP-0015-T6
status: todo
status: done
priority: medium
state_hub_task_id: "a7850784-3b86-453f-bbc7-1d53d0813f82"
```
@@ -119,3 +119,36 @@ prepared rights review, hat selection, admin preparation, and blocked journey.
- Headless and UI conformance tests.
- Security negative-path test suite.
- Adapter conformance harness for registration dependencies.
## Implementation Notes
Implemented on 2026-06-15:
- Extended `SCENARIO_MATRIX` and added `REGISTRATION_SCENARIO_MATRIX` covering
self-registration, prepared account claim, privileged role approval gates,
eID-backed assurance, family invite, tenant admin invite, group access, and
denied cross-tenant claim.
- Added `tests/test_registration_security_conformance.py` for a full local
registration -> prepared claim -> active hat -> claims projection ->
identity context -> access fact export -> onboarding -> UI diagnostics path.
- Added security negative-path tests for weak factor requirements, duplicate
identity links, prepared-account hijack attempts, expired claims,
cross-tenant/missing tenant context, privileged prepared-role approval, and
stale approval through approval-required access profiles.
- Added redaction and diagnostics checks for factor values, prepared-account
email metadata, sensitive profile values, access-profile claims/defaults,
and proofing adapter secrets.
- Added adapter conformance coverage for factor verification normalization,
authorization harness capture, access fact export, onboarding handoff/resume,
audit availability, outbox replay, and local durable-store behavior.
- Extended UI workflow coverage from USER-WP-0014 through the conformance
path and documented the local conformance contract in
`docs/registration-scenario-and-security-conformance.md`.
Verification:
```text
make test
Ran 75 tests in 1.506s
OK
```