Name the account site NetKingdom Identity.
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 40s
Account journey acceptance / journeys (push) Successful in 11s

Say "Signed in as" the identity, and describe a one-time code as a
higher security level of the NetKingdom sign-in rather than another
sign-in. The account site keeps its own session.

Assistant: grok
Assistant-Session: 01a0d25d-d358-7e13-b84a-d007fbb7e34f
This commit is contained in:
tegwick 2026-09-26 23:48:00 +02:00
parent b5c60ab2bd
commit b987a3de9e
9 changed files with 50 additions and 39 deletions

View file

@ -9,9 +9,11 @@ headless capability alone does not mean a journey is usable or verified live.
## Common interaction rules ## Common interaction rules
- The header states the verified portal identity, or “Not signed in to this - The header is titled NetKingdom Identity. It states “Signed in as” the
portal.” A valid portal session shows Log out; an absent/expired session shows verified identity, or “Not signed in.” A valid account-site session shows
Sign in. Never infer identity from URL parameters or an existing provider tab. Log out; an absent or expired session shows Sign in. A one-time code raises
the security level of the NetKingdom sign-in and is not another sign-in.
Never infer identity from URL parameters or an existing provider tab.
- The portal cannot observe every application or shared-provider session. Explain - The portal cannot observe every application or shared-provider session. Explain
this once in sign-out confirmation or expandable identity-switch help, not as this once in sign-out confirmation or expandable identity-switch help, not as
competing login/logout actions everywhere. “Use another account” remains competing login/logout actions everywhere. “Use another account” remains
@ -34,7 +36,7 @@ headless capability alone does not mean a journey is usable or verified live.
| ID / intent | Success | Failure and recovery | Current support / acceptance | | ID / intent | Success | Failure and recovery | Current support / acceptance |
|---|---|---|---| |---|---|---|---|
| U01 — Know whether I am signed in | Header and the home page name the verified portal identity, or say the portal session is absent. An application may keep its own session | Expired/unknown cookie shows signed-out state; a query does not invent a session; sign in again | Implemented; automated anonymous/expired/member/operator tests, including the home login-state section | | U01 — Know whether I am signed in | Header and the home page say “Signed in as” the verified identity, or that no account-site session exists. An application may keep its own session. A one-time code is a higher security level, not another sign-in | Expired/unknown cookie shows signed-out state; a query does not invent a session; sign in again | Implemented; automated anonymous/expired/member/operator tests, including the home identity section |
| U02 — Sign in to my company application | Personal login lands in the intended tenant and application | Wrong credentials stay on provider; denied membership leads to account help with identity switching | Recovery deployed previously; actual fresh-user acceptance waiting on OTP | | U02 — Sign in to my company application | Personal login lands in the intended tenant and application | Wrong credentials stay on provider; denied membership leads to account help with identity switching | Recovery deployed previously; actual fresh-user acceptance waiting on OTP |
| U03 — Accept an invitation | Confirm intended tenant/role, accept once, then see next setup step | Expired/used/wrong-person invitation explains next step; admin reissues without duplicates | Service/browser routes exist; live delivery and full browser acceptance pending | | U03 — Accept an invitation | Confirm intended tenant/role, accept once, then see next setup step | Expired/used/wrong-person invitation explains next step; admin reissues without duplicates | Service/browser routes exist; live delivery and full browser acceptance pending |
| U04 — Set or recover my password | Use actual login name, complete single-use setup, return to sign-in | Missing mail or expired link offers admin-assisted new setup link | Password setup reported successful; login name and sign-in address now named at handoff and in the user entry (2026-09-23 run, USER-WP-0035-T01); email delivery unresolved (USER-WP-0035-T02) | | U04 — Set or recover my password | Use actual login name, complete single-use setup, return to sign-in | Missing mail or expired link offers admin-assisted new setup link | Password setup reported successful; login name and sign-in address now named at handoff and in the user entry (2026-09-23 run, USER-WP-0035-T01); email delivery unresolved (USER-WP-0035-T02) |

View file

@ -19,7 +19,7 @@ def page(csrf, result=None):
failure = result.get('failure') failure = result.get('failure')
if failure: if failure:
messages = { messages = {
'fresh_platform_mfa_required': 'Verify your identity with a fresh MFA sign-in before viewing or changing policy.', 'fresh_platform_mfa_required': 'Raise the security level with a one-time code before viewing or changing policy.',
'preview_expired_or_changed': 'The review expired, changed or belongs to another session. Check the current policy and review again.', 'preview_expired_or_changed': 'The review expired, changed or belongs to another session. Check the current policy and review again.',
'policy_changed_review_again': 'Policy changed after this review. Check the current policy and review again.', 'policy_changed_review_again': 'Policy changed after this review. Check the current policy and review again.',
'reference_already_used': 'This reference is already recorded. Check the history; use a new reference for a new change.', 'reference_already_used': 'This reference is already recorded. Check the history; use a new reference for a new change.',

View file

@ -226,9 +226,9 @@ class PortalApplication:
actor = None actor = None
self._set_account_navigation(environ, actor) self._set_account_navigation(environ, actor)
identity = ( identity = (
f'<p>This portal is signed in as <strong>{escape(actor.preferred_username or actor.subject)}</strong>.</p>' f'<p>Signed in as <strong>{escape(actor.preferred_username or actor.subject)}</strong>.</p>'
'<p><a class="button" href="/onboarding">View my account and access</a></p>' '<p><a class="button" href="/onboarding">View my account and access</a></p>'
if actor else '<p>You are not signed in to this portal. Sign in to verify your identity and access.</p>' if actor else '<p>You are not signed in. Open the account site with your NetKingdom identity.</p>'
) )
return self._html(start_response, self._page_html( return self._html(start_response, self._page_html(
"Sign-in help", '<h1>Sign-in could not be completed</h1>' "Sign-in help", '<h1>Sign-in could not be completed</h1>'
@ -244,8 +244,8 @@ class PortalApplication:
return self._redirect(start_response, "/", correlation_id) return self._redirect(start_response, "/", correlation_id)
return self._html(start_response, self._page_html( return self._html(start_response, self._page_html(
"Not signed in", "Not signed in",
'<h1>You are not signed in to this portal.</h1>' '<h1>You are not signed in.</h1>'
'<p>Your shared NetKingdom sign-in may still be active. Signing in may reuse that account.</p>' '<p>Your shared NetKingdom sign-in may still be active. Opening the account site again may reuse that identity.</p>'
+ self._identity_switch_help(), + self._identity_switch_help(),
), correlation_id) ), correlation_id)
if path == "/logout" and method == "GET": if path == "/logout" and method == "GET":
@ -255,11 +255,11 @@ class PortalApplication:
self._set_account_navigation(environ, actor) self._set_account_navigation(environ, actor)
token = self._csrf_token(environ) token = self._csrf_token(environ)
return self._html(start_response, self._page_html( return self._html(start_response, self._page_html(
"Log out", '<h1>Log out of this portal?</h1>' "Log out", '<h1>End this account-site session?</h1>'
'<p>This ends your portal session. Your shared NetKingdom sign-in stays active.</p>' '<p>This ends the session on the account site. Your NetKingdom sign-in stays active.</p>'
'<form method="post" action="/logout">' '<form method="post" action="/logout">'
f'<input type="hidden" name="csrf_token" value="{escape(token)}">' f'<input type="hidden" name="csrf_token" value="{escape(token)}">'
'<button type="submit">Log out of this portal</button>' '<button type="submit">End this session</button>'
'<button type="submit" name="scope" value="shared">Continue to NetKingdom sign-out</button></form>', '<button type="submit" name="scope" value="shared">Continue to NetKingdom sign-out</button></form>',
), correlation_id) ), correlation_id)
if path == "/logout" and method == "POST": if path == "/logout" and method == "POST":
@ -1317,15 +1317,15 @@ class PortalApplication:
def _operation_capabilities(self) -> str: def _operation_capabilities(self) -> str:
capabilities = ( capabilities = (
("Portal sign-in", self.oidc_client is not None, "An existing portal session does not prove a fresh provider sign-in works."), ("Account-site session", self.oidc_client is not None, "An existing account-site session does not prove a fresh NetKingdom 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 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."), ("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."), ("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>' rows = "".join(f'<tr><td>{escape(name)}</td><td>{"Configured; live health unverified" if configured else "Unavailable on this account site"}</td><td>{escape(help_text)}</td></tr>'
for name, configured, help_text in capabilities) 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>' 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><a href="/platform/factor-recovery">Recover a lost authenticator</a></p>' if self.factor_recovery else '<p>Authenticator recovery is unavailable in this portal.</p>') + '<p><a href="/platform/authentication-policy">Review authentication policy</a>. A configured adapter is not a health check.</p></section>') + rows + '</tbody></table>' + ('<p><a href="/platform/factor-recovery">Recover a lost authenticator</a></p>' if self.factor_recovery else '<p>Authenticator recovery is unavailable on this account site.</p>') + '<p><a href="/platform/authentication-policy">Review authentication policy</a>. 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)
@ -1880,14 +1880,14 @@ class PortalApplication:
<h1>Sign-in security</h1> <h1>Sign-in security</h1>
<p>Use this page for help with your password and authenticator app.</p> <p>Use this page for help with your password and authenticator app.</p>
<section><h2>Two-step verification</h2> <section><h2>Two-step verification</h2>
<p>An authenticator app generates a short-lived code to enter after your password. <p>An authenticator app generates a short-lived code. Entering that code raises the security level of your NetKingdom sign-in. It is not another sign-in.
This portal cannot currently confirm whether an authenticator is enabled for your account.</p> This account site cannot currently confirm whether an authenticator is enabled for your account.</p>
""" + handoff + """ """ + handoff + """
<details><summary>How to set up an authenticator when setup is available</summary> <details><summary>How to set up an authenticator when setup is available</summary>
<ol><li>Open authenticator management and check that it shows your account.</li> <ol><li>Open authenticator management and check that it shows your account.</li>
<li>Choose Enroll Token, select TOTP, and scan its QR code with your authenticator app.</li> <li>Choose Enroll Token, select TOTP, and scan its QR code with your authenticator app.</li>
<li>Enter a current code to confirm setup. Wait for the sign-in service to confirm activation.</li> <li>Enter a current code to confirm setup. Wait for the sign-in service to confirm activation.</li>
<li>Follow the recovery instructions shown there, then test a new sign-in before closing your current session.</li></ol> <li>Follow the recovery instructions shown there, then confirm the new security level before closing your current session.</li></ol>
<p>Opening the setup page does not activate two-step verification. To cancel unfinished setup, open All Tokens, select the pending token, and choose Delete. A confirmed authenticator cannot be removed or replaced from this password-only management session; ask your administrator to use audited authenticator recovery.</p></details> <p>Opening the setup page does not activate two-step verification. To cancel unfinished setup, open All Tokens, select the pending token, and choose Delete. A confirmed authenticator cannot be removed or replaced from this password-only management session; ask your administrator to use audited authenticator recovery.</p></details>
<details><summary>A code is rejected, or I have lost my authenticator</summary> <details><summary>A code is rejected, or I have lost my authenticator</summary>
<p>Use the newest code for the correct account and check that your device sets its time automatically. <p>Use the newest code for the correct account and check that your device sets its time automatically.
@ -2197,19 +2197,25 @@ Use the login name they provide; it may differ from your display name.</p></sect
@staticmethod @staticmethod
def _login_state(actor: Any | None) -> str: def _login_state(actor: Any | None) -> str:
level = (
"<p>A one-time code raises the security level of a NetKingdom sign-in. "
"It is not another sign-in.</p>"
)
if actor is None: if actor is None:
return ( return (
'<section aria-labelledby="login-state"><h2 id="login-state">Login state</h2>' '<section aria-labelledby="login-state"><h2 id="login-state">Identity</h2>'
"<p>You are not signed in to this portal.</p></section>" "<p>You are not signed in.</p>"
f"{level}</section>"
) )
name = escape(actor.preferred_username or actor.subject) name = escape(actor.preferred_username or actor.subject)
verification = "Verified by your identity provider" if actor.assurance else "Verification pending" verification = "Verified by your identity provider" if actor.assurance else "Verification pending"
return ( return (
'<section aria-labelledby="login-state"><h2 id="login-state">Login state</h2>' '<section aria-labelledby="login-state"><h2 id="login-state">Identity</h2>'
f"<p>Signed in to this portal as <strong>{name}</strong>.</p>" f"<p>Signed in as <strong>{name}</strong>.</p>"
"<p>This is the portal session. An application can keep its own session.</p>" "<p>This session is for the account site. An application can keep its own session.</p>"
f"{level}"
f"<p>{escape(verification)}</p>" f"<p>{escape(verification)}</p>"
'<p><a href="/security">Password and two-step verification help</a></p></section>' '<p><a href="/security">Password and one-time code help</a></p></section>'
) )
@staticmethod @staticmethod
@ -2361,7 +2367,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
f'<p><a class="button" rel="noreferrer" href="{escape(setup_url)}">' f'<p><a class="button" rel="noreferrer" href="{escape(setup_url)}">'
"Continue to password setup</a></p>" "Continue to password setup</a></p>"
+ f'<p>After the password is set, sign in at <a href="{escape(self.login_url)}">{escape(self.login_url)}</a>. ' + f'<p>After the password is set, sign in at <a href="{escape(self.login_url)}">{escape(self.login_url)}</a>. '
"This portal does not deliver the setup link; pass it and the login name on yourself.</p>" "This account site does not deliver the setup link; pass it and the login name on yourself.</p>"
f'<p><a href="/admin/{escape(tenant)}">' f'<p><a href="/admin/{escape(tenant)}">'
"Return to tenant administration</a></p>", "Return to tenant administration</a></p>",
) )
@ -2372,7 +2378,7 @@ Use the login name they provide; it may differ from your display name.</p></sect
def _set_account_navigation(self, environ: Mapping[str, Any], actor: Any | None) -> None: def _set_account_navigation(self, environ: Mapping[str, Any], actor: Any | None) -> None:
if actor is None: if actor is None:
_ACCOUNT_NAVIGATION.set('<p>Not signed in to this portal</p><nav aria-label="Account navigation"><a href="/">Home</a><a href="/security">Sign-in help</a><a class="button" href="/login">Sign in</a></nav>') _ACCOUNT_NAVIGATION.set('<p>Not signed in</p><nav aria-label="Account navigation"><a href="/">Home</a><a href="/security">Sign-in help</a><a class="button" href="/login">Sign in</a></nav>')
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:
@ -2384,13 +2390,13 @@ Use the login name they provide; it may differ from your display name.</p></sect
if csrf: if csrf:
links += '<a href="/logout">Log out</a>' links += '<a href="/logout">Log out</a>'
identity = escape(actor.preferred_username or actor.subject) identity = escape(actor.preferred_username or actor.subject)
_ACCOUNT_NAVIGATION.set(f'<p>Signed in to this portal as <strong>{identity}</strong></p><nav aria-label="Account navigation">' + links + '</nav>') _ACCOUNT_NAVIGATION.set(f'<p>Signed in as <strong>{identity}</strong></p><nav aria-label="Account navigation">' + links + '</nav>')
@staticmethod @staticmethod
def _page_html(title: str, body: str) -> str: def _page_html(title: str, body: str) -> str:
return f"""<!doctype html><html lang="en"><head><meta charset="utf-8"> return f"""<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>{escape(title)} · Railiance</title><style> <title>{escape(title)} · NetKingdom Identity</title><style>
:root{{--ink:#17201c;--paper:#f5f1e8;--accent:#195b47;--line:#c8c1b3}} :root{{--ink:#17201c;--paper:#f5f1e8;--accent:#195b47;--line:#c8c1b3}}
*{{box-sizing:border-box}}body{{margin:0;background:var(--paper);color:var(--ink);font:18px/1.55 system-ui,sans-serif}} *{{box-sizing:border-box}}body{{margin:0;background:var(--paper);color:var(--ink);font:18px/1.55 system-ui,sans-serif}}
header,main{{max-width:68rem;margin:auto;padding:1.25rem}}header{{border-bottom:1px solid var(--line)}} header,main{{max-width:68rem;margin:auto;padding:1.25rem}}header{{border-bottom:1px solid var(--line)}}
@ -2401,7 +2407,7 @@ table{{width:100%;border-collapse:collapse;background:#fff}}th,td{{padding:.75re
section{{margin:2rem 0}}form{{display:grid;gap:.8rem;max-width:42rem}}label{{display:grid;gap:.25rem}} section{{margin:2rem 0}}form{{display:grid;gap:.8rem;max-width:42rem}}label{{display:grid;gap:.25rem}}
input,select,button{{font:inherit;padding:.65rem}}button{{background:var(--accent);color:white;border:0;border-radius:.3rem;cursor:pointer}} input,select,button{{font:inherit;padding:.65rem}}button{{background:var(--accent);color:white;border:0;border-radius:.3rem;cursor:pointer}}
a:focus-visible,input:focus-visible,select:focus-visible,button:focus-visible{{outline:3px solid #e59f24;outline-offset:3px}}@media(max-width:640px){{body{{font-size:16px}}table{{display:block;overflow-x:auto}}}} a:focus-visible,input:focus-visible,select:focus-visible,button:focus-visible{{outline:3px solid #e59f24;outline-offset:3px}}@media(max-width:640px){{body{{font-size:16px}}table{{display:block;overflow-x:auto}}}}
</style></head><body><header><strong>Railiance identity</strong>{_ACCOUNT_NAVIGATION.get()}</header><main>{body}</main></body></html>""" </style></head><body><header><strong>NetKingdom Identity</strong>{_ACCOUNT_NAVIGATION.get()}</header><main>{body}</main></body></html>"""
def _html( def _html(
self, start_response: StartResponse, body: str, correlation_id: str, self, start_response: StartResponse, body: str, correlation_id: str,

View file

@ -22,8 +22,10 @@ class AccountAwarenessTests(unittest.TestCase):
def test_signed_out_home_states_only_the_portal_session(self): def test_signed_out_home_states_only_the_portal_session(self):
_, body = invoke(self.app, "/", query="username=forged&tenant=tenant:evil:one") _, body = invoke(self.app, "/", query="username=forged&tenant=tenant:evil:one")
self.assertIn(b"Login state", body) self.assertIn(b"NetKingdom Identity", body)
self.assertIn(b"You are not signed in to this portal.", body) self.assertIn(b"Identity", body)
self.assertIn(b"You are not signed in.", body)
self.assertIn(b"It is not another sign-in.", body)
self.assertNotIn(b"Active now", body) self.assertNotIn(b"Active now", body)
self.assertNotIn(b"Allowed tenants", body) self.assertNotIn(b"Allowed tenants", body)
self.assertNotIn(b"forged", body) self.assertNotIn(b"forged", body)
@ -45,7 +47,8 @@ class AccountAwarenessTests(unittest.TestCase):
self.assertIn(b"Workload decisions are not checked.", body) self.assertIn(b"Workload decisions are not checked.", body)
self.assertNotIn(b"Viewing", body) self.assertNotIn(b"Viewing", body)
self.assertIn(b"An ordinary sign-in uses one tenant.", body) self.assertIn(b"An ordinary sign-in uses one tenant.", body)
self.assertIn(b"This is the portal session.", body) self.assertIn(b"This session is for the account site.", body)
self.assertIn(b"Signed in as", body)
def test_allowed_tenant_that_is_not_active_uses_sign_in(self): def test_allowed_tenant_that_is_not_active_uses_sign_in(self):
session = self.app.service.me(self.oidc.claims("member"), correlation_id="synthetic") session = self.app.service.me(self.oidc.claims("member"), correlation_id="synthetic")

View file

@ -17,7 +17,7 @@ class AccountClarityTests(unittest.TestCase):
self.assertIn(b'href="/login">Sign in', body) self.assertIn(b'href="/login">Sign in', body)
self.assertNotIn(b'href="/logout"', body) self.assertNotIn(b'href="/logout"', body)
self.assertNotIn(b'action="/logout"', body) self.assertNotIn(b'action="/logout"', body)
self.assertNotIn(b'Signed in to this portal as', body) self.assertNotIn(b'Signed in as', body)
self.assertNotIn(b'You have logged out', body) self.assertNotIn(b'You have logged out', body)
self.assertEqual('no-store', response['headers']['Cache-Control']) self.assertEqual('no-store', response['headers']['Cache-Control'])
@ -27,7 +27,7 @@ class AccountClarityTests(unittest.TestCase):
with self.subTest(path=path, who=who): with self.subTest(path=path, who=who):
response, body = invoke(self.app, path, cookie='ue_session='+who) response, body = invoke(self.app, path, cookie='ue_session='+who)
self.assertEqual('200 OK', response['status']) self.assertEqual('200 OK', response['status'])
self.assertIn(b'Signed in to this portal as', body) self.assertIn(b'Signed in as', body)
self.assertIn(b'href="/logout"', body) self.assertIn(b'href="/logout"', body)
self.assertNotIn(b'href="/login"', body) self.assertNotIn(b'href="/login"', body)
self.assertNotIn(b'Verify my current identity', body) self.assertNotIn(b'Verify my current identity', body)

View file

@ -17,9 +17,9 @@ class AccountRecoveryTests(unittest.TestCase):
response, body = self.get('/access-recovery') response, body = self.get('/access-recovery')
self.assertEqual('200 OK', response['status']) self.assertEqual('200 OK', response['status'])
self.assertIn(b'/onboarding', body) self.assertIn(b'/onboarding', body)
self.assertIn(b'This portal is signed in as', body) self.assertIn(b'Signed in as', body)
_, body = self.get('/onboarding') _, body = self.get('/onboarding')
self.assertIn(b'Login state', body) self.assertIn(b'Identity', body)
self.assertIn(b'Allowed workloads', body) self.assertIn(b'Allowed workloads', body)
self.assertIn(b'No workload access is recorded.', body) self.assertIn(b'No workload access is recorded.', body)
self.assertIn(b'Workload decisions are not checked.', body) self.assertIn(b'Workload decisions are not checked.', body)

View file

@ -42,7 +42,7 @@ class AuthenticationPolicyJourney(JourneyFixture):
self.assertEqual('403 Forbidden',response['status']) self.assertEqual('403 Forbidden',response['status'])
self.oidc.sessions['operator'].claims['assurance']['at']=time.time()-301 self.oidc.sessions['operator'].claims['assurance']['at']=time.time()-301
_,body=invoke(self.app,'/platform/authentication-policy',cookie='ue_session=operator') _,body=invoke(self.app,'/platform/authentication-policy',cookie='ue_session=operator')
self.assertIn(b'fresh MFA sign-in',body);self.assertEqual([],self.provider.calls) self.assertIn(b'one-time code before viewing',body);self.assertEqual([],self.provider.calls)
def test_review_explains_lockout_scope_rollback_and_receipts(self): def test_review_explains_lockout_scope_rollback_and_receipts(self):
_,body=self.post('/platform/authentication-policy',who='operator',action='preview',client='vergabe-demo-company',mode='mandatory',reference='p06-case') _,body=self.post('/platform/authentication-policy',who='operator',action='preview',client='vergabe-demo-company',mode='mandatory',reference='p06-case')
self.assertIn(b'Review policy change',body);self.assertIn(b'unable to complete sign-in',body) self.assertIn(b'Review policy change',body);self.assertIn(b'unable to complete sign-in',body)

View file

@ -74,6 +74,6 @@ class PlatformSupportJourneys(JourneyFixture):
response,body=invoke(self.app,"/platform/operations",cookie="ue_session=operator") response,body=invoke(self.app,"/platform/operations",cookie="ue_session=operator")
self.assertEqual("200 OK",response["status"]) self.assertEqual("200 OK",response["status"])
self.assertIn(b"Configured; live health unverified",body) self.assertIn(b"Configured; live health unverified",body)
self.assertIn(b"Unavailable in this portal",body) self.assertIn(b"Unavailable on this account site",body)
self.assertIn(b"assisted password setup",body) self.assertIn(b"assisted password setup",body)
self.assertIn(b"Review authentication policy",body) self.assertIn(b"Review authentication policy",body)

View file

@ -103,7 +103,7 @@ class PortalNavigationTests(unittest.TestCase):
def test_get_logout_only_confirms_and_bad_csrf_does_not_end_session(self): def test_get_logout_only_confirms_and_bad_csrf_does_not_end_session(self):
response, body = self.get('/logout') response, body = self.get('/logout')
self.assertEqual('200 OK',response['status']) self.assertEqual('200 OK',response['status'])
self.assertIn(b'Log out of this portal?',body) self.assertIn(b'End this account-site session?',body)
self.assertIsNotNone(self.oidc.claims('operator')) self.assertIsNotNone(self.oidc.claims('operator'))
for token in ['', 'wrong', 'member-csrf']: for token in ['', 'wrong', 'member-csrf']:
response,_=invoke(self.app,'/logout',method='POST',cookie='ue_session=operator',form={'csrf_token':token}) response,_=invoke(self.app,'/logout',method='POST',cookie='ue_session=operator',form={'csrf_token':token})