generated from coulomb/repo-seed
test: add registration security conformance
This commit is contained in:
@@ -16,6 +16,7 @@ See `docs/development.md`, `docs/configuration.md`, `docs/contracts.md`,
|
|||||||
`docs/hats-realms-services-assets-access-profiles.md`,
|
`docs/hats-realms-services-assets-access-profiles.md`,
|
||||||
`docs/onboarding-journeys-and-welcome-protocols.md`,
|
`docs/onboarding-journeys-and-welcome-protocols.md`,
|
||||||
`docs/registration-and-access-management-ui.md`, `docs/scenarios.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/operability.md`, `docs/release.md`, `docs/ui-contracts.md`,
|
||||||
`docs/identity-domain-naming-decision.md`, and `docs/final-assessment.md`
|
`docs/identity-domain-naming-decision.md`, and `docs/final-assessment.md`
|
||||||
for implementation boundaries, contracts, canon mappings, examples, and release
|
for implementation boundaries, contracts, canon mappings, examples, and release
|
||||||
|
|||||||
4
SCOPE.md
4
SCOPE.md
@@ -64,5 +64,5 @@ accounts and entitlement claims. `USER-WP-0012` implements hats, realms,
|
|||||||
services, assets, access profiles, active context, and exportable
|
services, assets, access profiles, active context, and exportable
|
||||||
access-control facts. `USER-WP-0013` implements onboarding journeys and
|
access-control facts. `USER-WP-0013` implements onboarding journeys and
|
||||||
welcome protocols. `USER-WP-0014` implements the optional registration and
|
welcome protocols. `USER-WP-0014` implements the optional registration and
|
||||||
access-management UI contract facade. `USER-WP-0015` remains proposed future
|
access-management UI contract facade. `USER-WP-0015` implements registration
|
||||||
work for security conformance.
|
scenario and security conformance tests.
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ accessible HTML verification. It does not handle credential entry, MFA
|
|||||||
challenges, token issuance, hidden policy decisions, notifications, or
|
challenges, token issuance, hidden policy decisions, notifications, or
|
||||||
service-specific admin consoles.
|
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 Contract
|
||||||
|
|
||||||
Registration is a headless user-entry facade. It creates a
|
Registration is a headless user-entry facade. It creates a
|
||||||
|
|||||||
@@ -236,8 +236,8 @@ once.
|
|||||||
## Recommended Workplans
|
## Recommended Workplans
|
||||||
|
|
||||||
As of 2026-06-15, `USER-WP-0010`, `USER-WP-0011`, `USER-WP-0012`,
|
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
|
`USER-WP-0013`, `USER-WP-0014`, and `USER-WP-0015` are implemented as
|
||||||
later security-conformance workplan remains recommended follow-on work.
|
user-engine slices.
|
||||||
|
|
||||||
| Workplan | Title | Purpose |
|
| Workplan | Title | Purpose |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
108
docs/registration-scenario-and-security-conformance.md
Normal file
108
docs/registration-scenario-and-security-conformance.md
Normal 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.
|
||||||
@@ -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. |
|
| 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. |
|
| 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. |
|
| 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
|
## Fixture Actors
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,67 @@ SCENARIO_MATRIX = (
|
|||||||
"two_applications",
|
"two_applications",
|
||||||
"sensitive_redaction",
|
"sensitive_redaction",
|
||||||
"audit_event_replay",
|
"audit_event_replay",
|
||||||
|
"identity_canon_context",
|
||||||
"family_dataspace_onboarding",
|
"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"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from user_engine.projections import ClaimsEnrichmentProjectionCache
|
|||||||
from user_engine.service import REDACTED, UserEngineService
|
from user_engine.service import REDACTED, UserEngineService
|
||||||
from user_engine.testing.fixtures import sample_application, sample_application_binding
|
from user_engine.testing.fixtures import sample_application, sample_application_binding
|
||||||
from user_engine.testing.scenarios import (
|
from user_engine.testing.scenarios import (
|
||||||
|
REGISTRATION_SCENARIO_MATRIX,
|
||||||
SCENARIO_MATRIX,
|
SCENARIO_MATRIX,
|
||||||
ScenarioAuthorizationHarness,
|
ScenarioAuthorizationHarness,
|
||||||
StrictFixtureIdentityClaimsAdapter,
|
StrictFixtureIdentityClaimsAdapter,
|
||||||
@@ -44,7 +45,29 @@ class IntegratedScenarioTests(unittest.TestCase):
|
|||||||
"two_applications",
|
"two_applications",
|
||||||
"sensitive_redaction",
|
"sensitive_redaction",
|
||||||
"audit_event_replay",
|
"audit_event_replay",
|
||||||
|
"identity_canon_context",
|
||||||
"family_dataspace_onboarding",
|
"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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
576
tests/test_registration_security_conformance.py
Normal file
576
tests/test_registration_security_conformance.py
Normal 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()
|
||||||
@@ -4,7 +4,7 @@ type: workplan
|
|||||||
title: "Registration Scenario And Security Conformance"
|
title: "Registration Scenario And Security Conformance"
|
||||||
domain: netkingdom
|
domain: netkingdom
|
||||||
repo: user-engine
|
repo: user-engine
|
||||||
status: proposed
|
status: finished
|
||||||
owner: codex
|
owner: codex
|
||||||
topic_slug: netkingdom
|
topic_slug: netkingdom
|
||||||
planning_priority: medium
|
planning_priority: medium
|
||||||
@@ -44,7 +44,7 @@ should cover both headless APIs and the optional UI surface where present.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T1
|
id: USER-WP-0015-T1
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "5ca0a269-559d-4138-b702-9984a411f2ed"
|
state_hub_task_id: "5ca0a269-559d-4138-b702-9984a411f2ed"
|
||||||
```
|
```
|
||||||
@@ -55,7 +55,7 @@ tenant admin invite, group access, and denied cross-tenant claim.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T2
|
id: USER-WP-0015-T2
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "6ee492b1-923f-4aa0-8e17-b69f522c4898"
|
state_hub_task_id: "6ee492b1-923f-4aa0-8e17-b69f522c4898"
|
||||||
```
|
```
|
||||||
@@ -65,7 +65,7 @@ claims enrichment, active hat selection, and onboarding event emission.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T3
|
id: USER-WP-0015-T3
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "b813a88f-ced6-40ce-9a25-d1c666fb73c9"
|
state_hub_task_id: "b813a88f-ced6-40ce-9a25-d1c666fb73c9"
|
||||||
```
|
```
|
||||||
@@ -76,7 +76,7 @@ privileged role escalation, and stale approvals.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T4
|
id: USER-WP-0015-T4
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "5a03ac1a-1f8e-455b-8f75-691e8bdda286"
|
state_hub_task_id: "5a03ac1a-1f8e-455b-8f75-691e8bdda286"
|
||||||
```
|
```
|
||||||
@@ -86,7 +86,7 @@ prepared-account metadata, active hat context, and access-profile evidence.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T5
|
id: USER-WP-0015-T5
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "fcf32b4d-d050-4989-bb05-844e0d13e548"
|
state_hub_task_id: "fcf32b4d-d050-4989-bb05-844e0d13e548"
|
||||||
```
|
```
|
||||||
@@ -97,7 +97,7 @@ durable store behavior.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: USER-WP-0015-T6
|
id: USER-WP-0015-T6
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "a7850784-3b86-453f-bbc7-1d53d0813f82"
|
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.
|
- Headless and UI conformance tests.
|
||||||
- Security negative-path test suite.
|
- Security negative-path test suite.
|
||||||
- Adapter conformance harness for registration dependencies.
|
- 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
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user