From b8506ef2e336f15eb85b3fd66072ddc7ade341c4 Mon Sep 17 00:00:00 2001
From: tegwick
Date: Sun, 13 Sep 2026 14:14:10 +0200
Subject: [PATCH] Add platform support investigation and clarify bounded
account recovery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
---
scripts/browser_journeys.mjs | 9 +++
src/user_engine/web.py | 68 +++++++++++++++-
tests/journey-coverage.json | 12 ++-
tests/test_platform_support.py | 79 +++++++++++++++++++
tests/test_web.py | 2 +-
.../USER-WP-0030-platform-admin-journeys.md | 12 +++
6 files changed, 174 insertions(+), 8 deletions(-)
create mode 100644 tests/test_platform_support.py
diff --git a/scripts/browser_journeys.mjs b/scripts/browser_journeys.mjs
index fa6847b..5d95c76 100644
--- a/scripts/browser_journeys.mjs
+++ b/scripts/browser_journeys.mjs
@@ -42,6 +42,15 @@ try{
await check(`!!document.querySelector('a[href="/platform/operations"]')`,'P01 platform recovery navigation');
await navigate('/platform/operations');
await check(`document.body.innerText.includes("Live sign-in, email receipt and authenticator health are not verified here")`,'P05 unknown provider health remains explicit');
+ await navigate('/platform/activity');
+ await check(`!!document.querySelector('input[name="reference"]') && !!document.querySelector('input[name="tenant"]')`,'P08 platform investigation filters');
+ await evaluate(`document.querySelector('input[name="reference"]').value='synthetic-missing';document.querySelector('form[action="/platform/activity"] button').click()`);
+ await waitFor('location.search.includes("synthetic-missing") && document.body.innerText.includes("No matching records")');
+ await check(`document.body.innerText.includes("does not prove that no action occurred")`,'P08 missing evidence is explicit');
+ await navigate('/admin/tenant:trial:demo-company');
+ await evaluate(`Array.from(document.forms).find(f=>f.action.endsWith("/recover")).querySelector("button").click()`);
+ await waitFor('document.body.innerText.includes("Confirm change")');
+ await check(`document.body.innerText.includes("does not reset a password") && document.body.innerText.includes("cannot bypass")`,'P04 restoration explains factor boundary');
await navigate('/logout');
await check(`document.body.innerText.includes("Log out of this portal?")`,'U11 logout requires confirmation');
await evaluate(`document.querySelector('form[action="/logout"] button').click()`);
diff --git a/src/user_engine/web.py b/src/user_engine/web.py
index fdde662..d2862f2 100644
--- a/src/user_engine/web.py
+++ b/src/user_engine/web.py
@@ -753,6 +753,14 @@ class PortalApplication:
"status": "removed", "tenant_account": _jsonable(account),
"provider_identity_removed": False,
}, correlation_id)
+ if path == "/platform/activity":
+ self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
+ if method != "GET":
+ raise NotFoundError("activity route not found")
+ self.service.tenant_diagnostics(actor, tenant=PLATFORM_TENANT, correlation_id=correlation_id)
+ query = parse_qs(str(environ.get("QUERY_STRING", "")))
+ return self._html(start_response, self._platform_activity(
+ query.get("reference", [""])[0], query.get("tenant", [""])[0]), correlation_id)
if path in {"/platform/operations", "/platform/operations/replay"}:
self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
if method == "POST" and path.endswith("/replay"):
@@ -921,12 +929,15 @@ class PortalApplication:
body = self._form_body(environ)
self._require_csrf(environ, str(body.get("csrf_token", "")))
if len(parts) == 6 and parts[3] == "users" and parts[5] in {"status", "remove", "recover", "role"}:
+ if parts[5] == "recover":
+ self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
user = self.service.store.user(parts[4])
state = self.service.store.tenant_account(tenant, parts[4])
snapshot = repr((state, self.service.store.memberships_for_user(parts[4], tenant=tenant)))
preview = self._confirm_change(environ, start_response, body, snapshot,
f"{parts[5].capitalize()} account in {tenant}",
- f"Account: {user.display_name or user.user_id}. This action applies to this tenant. Other tenant access and the shared login are retained.", correlation_id)
+ f"Account: {user.display_name or user.user_id}. This action applies to this tenant. Other tenant access and the shared login are retained."
+ + (" Recovery restores this tenant account and prepares a missing directory login. Verify the person's request through your established support process first. It does not reset a password, remove an authenticator, lift a global suspension, or prove account ownership. For a lost authenticator, use provider recovery; this action cannot bypass it." if parts[5] == "recover" else ""), correlation_id)
if preview is not None:
return preview
if len(parts) == 4 and parts[3] in {"users", "invitations"} and body.get("role", "user") not in {"user", "tenant-admin"}:
@@ -1155,6 +1166,42 @@ class PortalApplication:
if event.claimed_by: return "Being processed"
return "Queued for delivery"
+ def _platform_activity(self, reference: str, tenant: str) -> str:
+ reference, tenant = reference.strip(), tenant.strip()
+ if len(reference) > 200 or len(tenant) > 200:
+ raise ValidationError("Support reference and tenant must each be at most 200 characters.")
+ entries = []
+ for record in self.service.audit_records():
+ if (reference and record.correlation_id != reference) or (tenant and record.tenant != tenant):
+ continue
+ entries.append((record.recorded_at, "Audit", record.tenant, record.action,
+ record.actor.preferred_username or record.actor.subject, record.correlation_id,
+ "Recorded action; external outcome is unverified", ""))
+ for event in self.service.store.outbox_history():
+ if (reference and event.correlation_id != reference) or (tenant and event.tenant != tenant):
+ continue
+ link = "/platform/operations?" + urlencode({"event_id": event.event_id})
+ entries.append((event.occurred_at, "Delivery", event.tenant, event.event_type,
+ "—", event.correlation_id, self._delivery_status(event), link))
+ entries.sort(key=lambda row: row[0], reverse=True)
+ count = len(entries)
+ rows = ""
+ for stamp, kind, scope, action, actor_name, ref, status, link in entries[:100]:
+ detail = f'Inspect delivery' if link else ""
+ rows += "" + "".join(f"| {escape(value)} | " for value in
+ (stamp.isoformat(), kind, scope, action, actor_name, ref, status)) + f"{detail} |
"
+ empty = '| No matching records. This does not prove that no action occurred; check the reference and provider records. |
'
+ return self._page_html("Platform activity", f"""Platform activity
+Search an exact support reference across recorded tenant actions and delivery attempts. Add a full tenant identifier to narrow the scope.
+
+Showing {min(count, 100)} of {count} matching records, newest first. Filters apply before the 100-record display limit.
+| Time | Kind | Tenant | Action | Actor | Support reference | Known result | Next step |
{rows or empty}
+Audit records describe recorded actions. Delivery acceptance does not prove receipt, and neither proves a provider change or rollback. Check the relevant provider before closing an incident.
+Service recovery · Platform administration
""")
+
def _operations_page(self, actor: Any, csrf: str, event_id: str, correlation_id: str) -> str:
self.service.tenant_diagnostics(actor, tenant=PLATFORM_TENANT, correlation_id=correlation_id)
events = list(self.service.store.outbox_history())[-100:]
@@ -1168,12 +1215,25 @@ class PortalApplication:
if event.delivered_at is None and not event.claimed_by and (event.failed_at or event.dead_lettered_at):
action = f''
rows += f'| {escape(event.event_id)} | {escape(event.tenant)} | {escape(event.event_type)} | {escape(self._delivery_status(event))} | {escape(event.correlation_id)} | {action} |
'
- return self._page_html("Service recovery", 'Service recovery
This view shows local delivery records. Live sign-in, email receipt and authenticator health are not verified here.
'
+ configuration = self._operation_capabilities()
+ return self._page_html("Service recovery", 'Service recovery
' + configuration + 'Investigate a support reference
This view shows local delivery records. Live sign-in, email receipt and authenticator health are not verified here.
'
''
'| Delivery | Tenant | Kind | Status | Support reference | Recovery |
'
+ (rows or '| No delivery records. This does not prove mail was received. |
')
+ '
Queued retries are processed by the delivery worker. Check the record again for the result.
Return to platform administration
')
+ def _operation_capabilities(self) -> str:
+ capabilities = (
+ ("Portal sign-in", self.oidc_client is not None, "An existing portal session does not prove a fresh provider sign-in works."),
+ ("Tenant identity management", self.provisioning is not None, "Use tenant administration for login setup or tenant access recovery. Shared identity and factor recovery belong to the sign-in service."),
+ ("Tenant lifecycle", self.tenant_management is not None, "Review the authority's returned version after a change."),
+ ("Notification delivery", self.outbox_delivery is not None, "Inspect the delivery record below. If email cannot be received, use the tenant's assisted password setup process."),
+ )
+ rows = "".join(f'| {escape(name)} | {"Configured; live health unverified" if configured else "Unavailable in this portal"} | {escape(help_text)} |
'
+ for name, configured, help_text in capabilities)
+ return ('Service capabilities
| Service | Known state | Recovery step |
'
+ + rows + '
Authenticator recovery and authentication policy changes are unavailable in this portal. The sign-in service owner must verify factor lookup, recovery and policy enforcement. A configured adapter is not a health check.
')
+
def _require_setup_access(self, tenant: str, user_id: str) -> None:
account = self.service.store.tenant_account(tenant, user_id)
if account is not None and account.status in {AccountStatus.SUSPENDED, AccountStatus.DISABLED}:
@@ -1913,7 +1973,7 @@ Use the login name they provide; it may differ from your display name.
{escape(user.display_name or membership.user_id) if user else escape(membership.user_id)} | '
f'{escape(user.primary_email or "") if user else ""} | {escape(membership.kind)} | '
f'{escape(status.value)} for this tenant | {login} | {actions} | ')
@@ -2113,7 +2173,7 @@ Use the login name they provide; it may differ from your display name.HomeMy accountSign-in security'
if "platform-operator" in actor.roles:
- links += 'Platform administrationService recovery'
+ links += 'Platform administrationService recoveryPlatform activity'
elif "tenant-admin" in actor.roles:
links += f'Manage users'
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
diff --git a/tests/journey-coverage.json b/tests/journey-coverage.json
index 6adb02d..403093f 100644
--- a/tests/journey-coverage.json
+++ b/tests/journey-coverage.json
@@ -245,7 +245,8 @@
"role": "platform_admin",
"implementation": "external-blocked",
"tests": [
- "test_journey_roles.PlatformAdminJourneys.test_recovery_uses_tenant_access_and_keeps_global_identity_operations_unused"
+ "test_journey_roles.PlatformAdminJourneys.test_recovery_uses_tenant_access_and_keeps_global_identity_operations_unused",
+ "test_platform_support.PlatformSupportJourneys.test_recovery_denied_before_preview_and_operator_sees_factor_boundary"
],
"remaining": "Tenant identity recovery is scoped; verified OTP/account-ownership recovery remains provider-owned."
},
@@ -254,7 +255,8 @@
"role": "platform_admin",
"implementation": "external-blocked",
"tests": [
- "test_journey_roles.PlatformAdminJourneys.test_delivery_denial_redaction_retry_and_completed_guard"
+ "test_journey_roles.PlatformAdminJourneys.test_delivery_denial_redaction_retry_and_completed_guard",
+ "test_platform_support.PlatformSupportJourneys.test_service_capabilities_distinguish_configuration_from_health"
],
"remaining": "Local delivery record operations work; approved factor credential renewal and mail receipt remain external dependencies."
},
@@ -283,7 +285,11 @@
"implementation": "implemented",
"tests": [
"test_journey_roles.PlatformAdminJourneys.test_delivery_denial_redaction_retry_and_completed_guard",
- "test_journey_roles.TenantAdminJourneys.test_audit_is_tenant_scoped_and_never_dumps_payload"
+ "test_journey_roles.TenantAdminJourneys.test_audit_is_tenant_scoped_and_never_dumps_payload",
+ "test_platform_support.PlatformSupportJourneys.test_operator_correlates_actions_and_delivery_without_raw_content",
+ "test_platform_support.PlatformSupportJourneys.test_activity_denies_nonoperators_and_mutations",
+ "test_platform_support.PlatformSupportJourneys.test_exact_filters_apply_before_display_limit_and_missing_is_explicit",
+ "test_platform_support.PlatformSupportJourneys.test_filter_values_are_escaped_and_bounded"
],
"remaining": ""
}
diff --git a/tests/test_platform_support.py b/tests/test_platform_support.py
new file mode 100644
index 0000000..46f5b23
--- /dev/null
+++ b/tests/test_platform_support.py
@@ -0,0 +1,79 @@
+"""Platform support journeys with synthetic records and provider failures."""
+from dataclasses import replace
+from urllib.parse import urlencode
+from user_engine.domain import AuditRecord, OutboxEvent, utc_now
+from test_journey_roles import JourneyFixture, TENANT, OTHER
+from test_web import invoke
+
+class PlatformSupportJourneys(JourneyFixture):
+ def seed(self):
+ self.app.service.store.append_audit(AuditRecord(audit_id="audit-support", actor=self.actor,
+ action="tenant.account.update", subject="person", tenant=TENANT,
+ correlation_id="support-123", summary="private-audit-summary"))
+ self.app.service.store.append_outbox(OutboxEvent(event_id="support-delivery", event_type="notification.requested",
+ aggregate_id="person", tenant=TENANT, correlation_id="support-123",
+ payload={"private":"private-payload"}, failed_at=utc_now(), failure_reason="private-provider-error"))
+
+ def test_operator_correlates_actions_and_delivery_without_raw_content(self):
+ self.seed()
+ self.app.service.store.append_outbox(OutboxEvent(event_id="unrelated", event_type="private-unrelated",
+ aggregate_id="other", tenant=OTHER, correlation_id="different", payload={}))
+ response,body=invoke(self.app,"/platform/activity",cookie="ue_session=operator",query="reference=support-123")
+ self.assertEqual("200 OK",response["status"])
+ for value in [b"tenant.account.update",b"notification.requested",b"Inspect delivery",b"support-delivery",b"Delivery failed"]:
+ self.assertIn(value,body)
+ for value in [b"private-audit-summary",b"private-payload",b"private-provider-error",b"private-unrelated"]:
+ self.assertNotIn(value,body)
+ self.assertIn(b"external outcome is unverified",body)
+
+ def test_activity_denies_nonoperators_and_mutations(self):
+ self.seed()
+ for who in ["member","admin"]:
+ response,body=invoke(self.app,"/platform/activity",cookie="ue_session="+who)
+ self.assertEqual("403 Forbidden",response["status"])
+ self.assertNotIn(b"support-delivery",body)
+ response,_=self.post("/platform/activity",who="operator")
+ self.assertEqual("404 Not Found",response["status"])
+
+ def test_exact_filters_apply_before_display_limit_and_missing_is_explicit(self):
+ self.seed()
+ for i in range(105):
+ self.app.service.store.append_outbox(OutboxEvent(event_id="noise-"+str(i),event_type="noise",
+ aggregate_id="other",tenant=OTHER,correlation_id="noise",payload={}))
+ response,body=invoke(self.app,"/platform/activity",cookie="ue_session=operator",
+ query=urlencode({"reference":"support-123","tenant":TENANT}))
+ self.assertEqual("200 OK",response["status"])
+ self.assertIn(b"support-delivery",body)
+ self.assertIn(b"Showing 2 of 2",body)
+ for query in ["reference=support",urlencode({"reference":"support-123","tenant":OTHER})]:
+ _,body=invoke(self.app,"/platform/activity",cookie="ue_session=operator",query=query)
+ self.assertIn(b"No matching records",body)
+ self.assertNotIn(b"support-delivery",body)
+
+ def test_filter_values_are_escaped_and_bounded(self):
+ response,body=invoke(self.app,"/platform/activity",cookie="ue_session=operator",query=urlencode({"reference":""}))
+ self.assertEqual("200 OK",response["status"])
+ self.assertIn(b"<script>",body);self.assertNotIn(b"