Add platform support investigation and clarify bounded account recovery
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
a7fba14307
commit
b8506ef2e3
6 changed files with 174 additions and 8 deletions
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue