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
|
|
@ -42,6 +42,15 @@ try{
|
||||||
await check(`!!document.querySelector('a[href="/platform/operations"]')`,'P01 platform recovery navigation');
|
await check(`!!document.querySelector('a[href="/platform/operations"]')`,'P01 platform recovery navigation');
|
||||||
await navigate('/platform/operations');
|
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 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 navigate('/logout');
|
||||||
await check(`document.body.innerText.includes("Log out of this portal?")`,'U11 logout requires confirmation');
|
await check(`document.body.innerText.includes("Log out of this portal?")`,'U11 logout requires confirmation');
|
||||||
await evaluate(`document.querySelector('form[action="/logout"] button').click()`);
|
await evaluate(`document.querySelector('form[action="/logout"] button').click()`);
|
||||||
|
|
|
||||||
|
|
@ -753,6 +753,14 @@ class PortalApplication:
|
||||||
"status": "removed", "tenant_account": _jsonable(account),
|
"status": "removed", "tenant_account": _jsonable(account),
|
||||||
"provider_identity_removed": False,
|
"provider_identity_removed": False,
|
||||||
}, correlation_id)
|
}, 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"}:
|
if path in {"/platform/operations", "/platform/operations/replay"}:
|
||||||
self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
|
self.service.resolve_tenant_context(actor, PLATFORM_TENANT)
|
||||||
if method == "POST" and path.endswith("/replay"):
|
if method == "POST" and path.endswith("/replay"):
|
||||||
|
|
@ -921,12 +929,15 @@ class PortalApplication:
|
||||||
body = self._form_body(environ)
|
body = self._form_body(environ)
|
||||||
self._require_csrf(environ, str(body.get("csrf_token", "")))
|
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 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])
|
user = self.service.store.user(parts[4])
|
||||||
state = self.service.store.tenant_account(tenant, parts[4])
|
state = self.service.store.tenant_account(tenant, parts[4])
|
||||||
snapshot = repr((state, self.service.store.memberships_for_user(parts[4], tenant=tenant)))
|
snapshot = repr((state, self.service.store.memberships_for_user(parts[4], tenant=tenant)))
|
||||||
preview = self._confirm_change(environ, start_response, body, snapshot,
|
preview = self._confirm_change(environ, start_response, body, snapshot,
|
||||||
f"{parts[5].capitalize()} account in {tenant}",
|
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:
|
if preview is not None:
|
||||||
return preview
|
return preview
|
||||||
if len(parts) == 4 and parts[3] in {"users", "invitations"} and body.get("role", "user") not in {"user", "tenant-admin"}:
|
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"
|
if event.claimed_by: return "Being processed"
|
||||||
return "Queued for delivery"
|
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:
|
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)
|
self.service.tenant_diagnostics(actor, tenant=PLATFORM_TENANT, correlation_id=correlation_id)
|
||||||
events = list(self.service.store.outbox_history())[-100:]
|
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):
|
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>'
|
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>'
|
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>'
|
'<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>'
|
'<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>')
|
+ (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>')
|
+ '</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:
|
def _require_setup_access(self, tenant: str, user_id: str) -> None:
|
||||||
account = self.service.store.tenant_account(tenant, user_id)
|
account = self.service.store.tenant_account(tenant, user_id)
|
||||||
if account is not None and account.status in {AccountStatus.SUSPENDED, AccountStatus.DISABLED}:
|
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")
|
actions += form("remove", "Remove account")
|
||||||
next_role = "user" if membership.kind == "tenant-admin" else "tenant-admin"
|
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)
|
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>'
|
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(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>')
|
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
|
return
|
||||||
links = '<a href="/">Home</a><a href="/onboarding">My account</a><a href="/security">Sign-in security</a>'
|
links = '<a href="/">Home</a><a href="/onboarding">My account</a><a href="/security">Sign-in security</a>'
|
||||||
if "platform-operator" in actor.roles:
|
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:
|
elif "tenant-admin" in actor.roles:
|
||||||
links += f'<a href="/admin/{escape(quote(actor.tenant, safe=""))}">Manage users</a>'
|
links += f'<a href="/admin/{escape(quote(actor.tenant, safe=""))}">Manage users</a>'
|
||||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,8 @@
|
||||||
"role": "platform_admin",
|
"role": "platform_admin",
|
||||||
"implementation": "external-blocked",
|
"implementation": "external-blocked",
|
||||||
"tests": [
|
"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."
|
"remaining": "Tenant identity recovery is scoped; verified OTP/account-ownership recovery remains provider-owned."
|
||||||
},
|
},
|
||||||
|
|
@ -254,7 +255,8 @@
|
||||||
"role": "platform_admin",
|
"role": "platform_admin",
|
||||||
"implementation": "external-blocked",
|
"implementation": "external-blocked",
|
||||||
"tests": [
|
"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."
|
"remaining": "Local delivery record operations work; approved factor credential renewal and mail receipt remain external dependencies."
|
||||||
},
|
},
|
||||||
|
|
@ -283,7 +285,11 @@
|
||||||
"implementation": "implemented",
|
"implementation": "implemented",
|
||||||
"tests": [
|
"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_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": ""
|
"remaining": ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
tests/test_platform_support.py
Normal file
79
tests/test_platform_support.py
Normal file
|
|
@ -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":"<script>alert(1)</script>"}))
|
||||||
|
self.assertEqual("200 OK",response["status"])
|
||||||
|
self.assertIn(b"<script>",body);self.assertNotIn(b"<script>",body)
|
||||||
|
response,_=invoke(self.app,"/platform/activity",cookie="ue_session=operator",query="reference="+"x"*201)
|
||||||
|
self.assertEqual("400 Bad Request",response["status"])
|
||||||
|
|
||||||
|
def test_recovery_denied_before_preview_and_operator_sees_factor_boundary(self):
|
||||||
|
user=self.member(role="tenant-admin")
|
||||||
|
path=f"/admin/{TENANT}/users/{user.user_id}/recover"
|
||||||
|
response,body=self.post(path,who="admin")
|
||||||
|
self.assertEqual("403 Forbidden",response["status"])
|
||||||
|
self.assertNotIn(b"Confirm change",body)
|
||||||
|
response,body=self.post(path,who="operator")
|
||||||
|
self.assertEqual("200 OK",response["status"])
|
||||||
|
self.assertIn(b"does not reset a password",body)
|
||||||
|
self.assertIn(b"cannot bypass",body)
|
||||||
|
self.assertEqual([],self.app.provisioning.actions)
|
||||||
|
|
||||||
|
def test_service_capabilities_distinguish_configuration_from_health(self):
|
||||||
|
self.app.provisioning=None;self.app.tenant_management=None;self.app.outbox_delivery=None
|
||||||
|
response,body=invoke(self.app,"/platform/operations",cookie="ue_session=operator")
|
||||||
|
self.assertEqual("200 OK",response["status"])
|
||||||
|
self.assertIn(b"Configured; live health unverified",body)
|
||||||
|
self.assertIn(b"Unavailable in this portal",body)
|
||||||
|
self.assertIn(b"assisted password setup",body)
|
||||||
|
self.assertIn(b"authentication policy changes are unavailable",body)
|
||||||
|
|
@ -976,7 +976,7 @@ class PortalApplicationTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual("200 OK", admin_page["status"])
|
self.assertEqual("200 OK", admin_page["status"])
|
||||||
self.assertIn(b"Lifecycle diagnostics", html)
|
self.assertIn(b"Lifecycle diagnostics", html)
|
||||||
self.assertIn(b"Recover identity", html)
|
self.assertIn(b"Restore tenant account", html)
|
||||||
recovered, _ = invoke_confirmed(
|
recovered, _ = invoke_confirmed(
|
||||||
self.app,
|
self.app,
|
||||||
f"/admin/tenant:friendly:browser/users/{memberships[0].user_id}/recover",
|
f"/admin/tenant:friendly:browser/users/{memberships[0].user_id}/recover",
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,15 @@ Validation: 210 database-enabled regression tests passed with no skips,
|
||||||
including independent-connection last-admin protection and nested bootstrap
|
including independent-connection last-admin protection and nested bootstrap
|
||||||
rollback. Thirteen isolated Chromium checks passed. Provider OTP and application
|
rollback. Thirteen isolated Chromium checks passed. Provider OTP and application
|
||||||
access integration remain explicitly open; no complete-journey claim is inferred.
|
access integration remain explicitly open; no complete-journey claim is inferred.
|
||||||
|
|
||||||
|
## Finish platform support investigation and recovery clarity
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: USER-WP-0030-T04
|
||||||
|
status: progress
|
||||||
|
priority: high
|
||||||
|
```
|
||||||
|
|
||||||
|
Prioritize P04/P05/P08: exact support-reference search across authorized platform audit and delivery metadata, tenant narrowing before display limits, honest missing-evidence state, delivery readback links and actionable capability availability. Deny non-operators before recovery preview and explain tenant restoration versus provider factor/account-ownership recovery. Add regression and browser acceptance, publish and verify.
|
||||||
|
|
||||||
|
Provider gate rechecked: net-kingdom-privacyidea-admin-token remains non-resolvable. The owner playbook in ops-warden/wiki/playbooks/net-kingdom-sso-bind-credentials.md requires a concrete custody/renewal contract and approved attended action; it does not authorize reading live Secrets. P04 factor recovery/P05 credential operations/P06 effective policy remain T03 until that contract exists.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue