USER-WP-0026-T03: access-engine application access readback client
Read FLEX-DEC-2026-019 readback answers for workloads bound in all three records (user-engine application, protected system, key-cape client) and show them on the account page. No row is inferred as allow; failures, missing rows and expired allows render 'could not be checked'. Off unless USER_ENGINE_READBACK_URL and its own token file are set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 629164@bnt-lap001 Assistant-Session: 22dee475-6a23-47f3-8588-00a7f62d8720
This commit is contained in:
parent
119910640f
commit
869de4080b
11 changed files with 780 additions and 2 deletions
|
|
@ -94,6 +94,7 @@ class PortalApplication:
|
|||
identity_lookup: Any | None = None,
|
||||
hub_authority_caller: Actor | None = None,
|
||||
hub_authority_token: str = "",
|
||||
application_access: Any | None = None,
|
||||
) -> None:
|
||||
if len(trusted_proxy_secret) < 24:
|
||||
raise ValueError("trusted proxy secret must contain at least 24 characters")
|
||||
|
|
@ -136,6 +137,7 @@ class PortalApplication:
|
|||
raise ValueError("hub authority lookup needs both a caller and a token, or neither")
|
||||
self.hub_authority_caller = hub_authority_caller
|
||||
self.hub_authority_token = hub_authority_token
|
||||
self.application_access = application_access
|
||||
|
||||
def __call__(self, environ: Mapping[str, Any], start_response: StartResponse) -> Iterable[bytes]:
|
||||
correlation_id = environ.get("HTTP_X_REQUEST_ID") or f"corr_{secrets.token_hex(12)}"
|
||||
|
|
@ -311,6 +313,8 @@ class PortalApplication:
|
|||
if path == "/logout" and method == "POST":
|
||||
session_id = cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session")
|
||||
body = self._form_body(environ)
|
||||
if session_id and self.application_access is not None:
|
||||
self.application_access.forget(session_id)
|
||||
if session_id and self.oidc_client and self.oidc_client.claims(session_id) is not None:
|
||||
self._require_csrf(environ, str(body.get("csrf_token", "")))
|
||||
self.oidc_client.logout(session_id)
|
||||
|
|
@ -444,6 +448,10 @@ class PortalApplication:
|
|||
start_response,
|
||||
self._onboarding(
|
||||
session, memberships, journeys, self._csrf_token(environ),
|
||||
application_access=self._application_access_section(
|
||||
session.actor,
|
||||
cookie_value(str(environ.get("HTTP_COOKIE", "")), "ue_session") or "",
|
||||
),
|
||||
),
|
||||
correlation_id,
|
||||
)
|
||||
|
|
@ -2273,7 +2281,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
|||
|
||||
def _onboarding(
|
||||
self, session: Any, memberships: tuple[Any, ...], journeys: tuple[Any, ...],
|
||||
csrf_token: str,
|
||||
csrf_token: str, application_access: str = "",
|
||||
) -> str:
|
||||
journey_items = "".join(
|
||||
self._onboarding_journey_item(item, csrf_token) for item in journeys
|
||||
|
|
@ -2292,6 +2300,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
|||
<button type="submit">Save profile</button></form></section>
|
||||
{self._allowed_tenants(actor, memberships)}
|
||||
{self._allowed_workloads(memberships)}
|
||||
{application_access}
|
||||
<section aria-labelledby="steps"><h2 id="steps">Onboarding progress</h2><ul>{journey_items}</ul></section>""",
|
||||
)
|
||||
|
||||
|
|
@ -2426,6 +2435,35 @@ Use the login name they provide; it may differ from your display name.</p></sect
|
|||
"<p>Each application checks access when you open it. Tenant membership alone does not grant access to every application.</p></section>"
|
||||
)
|
||||
|
||||
_APPLICATION_ACCESS_TEXT = {
|
||||
"available": "Available",
|
||||
"denied": "Not available",
|
||||
"not_offered": "Not available: not offered here",
|
||||
"unchecked": "Not available: could not be checked",
|
||||
}
|
||||
|
||||
def _application_access_section(self, actor: Any, session_key: str) -> str:
|
||||
"""Render access-engine readback answers; never infer an allow."""
|
||||
if self.application_access is None:
|
||||
return ""
|
||||
rows = self.application_access.rows(actor, session_key)
|
||||
if rows is None:
|
||||
items = "<li>Application access could not be checked. Try again later.</li>"
|
||||
elif not rows:
|
||||
items = "<li>No application is registered for an access check.</li>"
|
||||
else:
|
||||
items = "".join(
|
||||
f'<li data-access="{escape(row.status)}">{escape(row.workload.display_name)} - '
|
||||
f"{escape(self._APPLICATION_ACCESS_TEXT[row.status])}</li>"
|
||||
for row in rows
|
||||
)
|
||||
return (
|
||||
'<section aria-labelledby="application-access"><h2 id="application-access">Application access</h2>'
|
||||
f'<ul id="application-access-list">{items}</ul>'
|
||||
"<p>Checked with the access service for your current sign-in. Each application checks again when you open it. "
|
||||
"This list covers registered applications only and is not a complete inventory.</p></section>"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _onboarding_journey_item(journey: Any, csrf_token: str) -> str:
|
||||
steps = "".join(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue