Add platform support investigation and clarify bounded account recovery
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 33s
Account journey acceptance / journeys (push) Successful in 5s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
tegwick 2026-09-13 14:14:10 +02:00
parent a7fba14307
commit b8506ef2e3
6 changed files with 174 additions and 8 deletions

View file

@ -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'<a href="{escape(link)}">Inspect delivery</a>' if link else ""
rows += "<tr>" + "".join(f"<td>{escape(value)}</td>" for value in
(stamp.isoformat(), kind, scope, action, actor_name, ref, status)) + f"<td>{detail}</td></tr>"
empty = '<tr><td colspan="8">No matching records. This does not prove that no action occurred; check the reference and provider records.</td></tr>'
return self._page_html("Platform activity", f"""<h1>Platform activity</h1>
<p>Search an exact support reference across recorded tenant actions and delivery attempts. Add a full tenant identifier to narrow the scope.</p>
<form method="get" action="/platform/activity">
<label>Support reference <input name="reference" maxlength="200" value="{escape(reference)}"></label>
<label>Tenant identifier <input name="tenant" maxlength="200" value="{escape(tenant)}"></label>
<button type="submit">Find activity</button></form>
<p>Showing {min(count, 100)} of {count} matching records, newest first. Filters apply before the 100-record display limit.</p>
<table><thead><tr><th>Time</th><th>Kind</th><th>Tenant</th><th>Action</th><th>Actor</th><th>Support reference</th><th>Known result</th><th>Next step</th></tr></thead><tbody>{rows or empty}</tbody></table>
<p>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.</p>
<p><a href="/platform/operations">Service recovery</a> · <a href="/platform">Platform administration</a></p>""")
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'<form method="post" action="/platform/operations/replay"><input type="hidden" name="csrf_token" value="{escape(csrf)}"><input type="hidden" name="event_id" value="{escape(event.event_id)}"><button type="submit">Queue a retry</button></form>'
rows += f'<tr><td>{escape(event.event_id)}</td><td>{escape(event.tenant)}</td><td>{escape(event.event_type)}</td><td>{escape(self._delivery_status(event))}</td><td>{escape(event.correlation_id)}</td><td>{action}</td></tr>'
return self._page_html("Service recovery", '<h1>Service recovery</h1><p>This view shows local delivery records. Live sign-in, email receipt and authenticator health are not verified here.</p>'
configuration = self._operation_capabilities()
return self._page_html("Service recovery", '<h1>Service recovery</h1>' + configuration + '<p><a href="/platform/activity">Investigate a support reference</a></p><p>This view shows local delivery records. Live sign-in, email receipt and authenticator health are not verified here.</p>'
'<form method="get" action="/platform/operations"><label>Delivery record ID <input name="event_id"></label><button type="submit">Find delivery</button></form>'
'<table><thead><tr><th>Delivery</th><th>Tenant</th><th>Kind</th><th>Status</th><th>Support reference</th><th>Recovery</th></tr></thead><tbody>'
+ (rows or '<tr><td colspan="6">No delivery records. This does not prove mail was received.</td></tr>')
+ '</tbody></table><p>Queued retries are processed by the delivery worker. Check the record again for the result.</p><p><a href="/platform">Return to platform administration</a></p>')
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'<tr><td>{escape(name)}</td><td>{"Configured; live health unverified" if configured else "Unavailable in this portal"}</td><td>{escape(help_text)}</td></tr>'
for name, configured, help_text in capabilities)
return ('<section><h2>Service capabilities</h2><table><thead><tr><th>Service</th><th>Known state</th><th>Recovery step</th></tr></thead><tbody>'
+ rows + '</tbody></table><p>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.</p></section>')
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.</p></sect
actions += form("remove", "Remove account")
next_role = "user" if membership.kind == "tenant-admin" else "tenant-admin"
actions += form("role", "Make user" if next_role == "user" else "Make tenant administrator", role=next_role)
if platform_operator: actions += form("recover", "Recover identity")
if platform_operator: actions += form("recover", "Restore tenant account")
return (f'<tr><td>{escape(user.display_name or membership.user_id) if user else escape(membership.user_id)}</td>'
f'<td>{escape(user.primary_email or "") if user else ""}</td><td>{escape(membership.kind)}</td>'
f'<td>{escape(status.value)} for this tenant</td><td>{login}</td><td>{actions}</td></tr>')
@ -2113,7 +2173,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
return
links = '<a href="/">Home</a><a href="/onboarding">My account</a><a href="/security">Sign-in security</a>'
if "platform-operator" in actor.roles:
links += '<a href="/platform">Platform administration</a><a href="/platform/operations">Service recovery</a>'
links += '<a href="/platform">Platform administration</a><a href="/platform/operations">Service recovery</a><a href="/platform/activity">Platform activity</a>'
elif "tenant-admin" in actor.roles:
links += f'<a href="/admin/{escape(quote(actor.tenant, safe=""))}">Manage users</a>'
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")