user-engine/tests/test_application_access.py
tegwick 869de4080b 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
2026-10-10 00:45:03 +02:00

330 lines
13 KiB
Python

import io
import json
import os
import tempfile
import unittest
from datetime import UTC, datetime, timedelta
from unittest.mock import patch
from urllib.error import HTTPError, URLError
from user_engine.adapters.application_access import ApplicationAccessReadbackHTTPAdapter
from user_engine.adapters.flex_auth import FlexAuthHTTPAdapter
from user_engine.application_access import (
ApplicationAccessReader,
ReadbackResult,
ReadbackSystem,
ReadbackUnavailable,
Workload,
render_rows,
workload_catalogue,
)
from user_engine.domain import (
Actor,
Application,
ApplicationBinding,
AuthorizationRequest,
PrincipalType,
)
ISSUER = "https://key-cape.example/realms/coulomb"
NOW = datetime(2026, 10, 10, 12, 0, tzinfo=UTC)
class _Response(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *_args):
self.close()
def _actor(subject="subject-1"):
return Actor(
issuer=ISSUER, subject=subject, tenant="tenant-a",
principal_type=PrincipalType.HUMAN, audience=("user-engine",),
roles=("member",), tenant_roles=("CUS",), groups=("tenant:a:users",),
scopes=("openid",), assurance={"level": "aal2", "mfa": True},
)
def _bound(app_id, system_id, client_id):
return (
Application(application_id=app_id, display_name=app_id.title(), owner="owner"),
ApplicationBinding(application_id=app_id, oidc_client_id=client_id,
protected_system_id=system_id),
)
def _system(system_id, client_id, issuer=ISSUER):
return ReadbackSystem(system_id=system_id, name=system_id, readback=True,
client_issuer=issuer, client_id=client_id)
class WorkloadCatalogueTests(unittest.TestCase):
def test_lists_only_workloads_bound_in_all_three_records(self):
applications = [
_bound("informed", "informed-decision", "informed-client"),
_bound("unbound", None, "unbound-client"),
_bound("wrongclient", "wrong-client", "client-a"),
_bound("wrongissuer", "other-issuer", "client-b"),
_bound("nobinding", "no-client-binding", "client-c"),
_bound("unknown", "not-in-registry", "client-d"),
]
systems = [
_system("informed-decision", "informed-client"),
_system("wrong-client", "client-z"),
_system("other-issuer", "client-b", issuer="https://elsewhere"),
ReadbackSystem("no-client-binding", "x", readback=True),
]
workloads = workload_catalogue(applications, systems, issuer=ISSUER + "/")
self.assertEqual([item.system_id for item in workloads], ["informed-decision"])
class RenderTests(unittest.TestCase):
workloads = tuple(Workload(f"app-{n}", f"App {n}", f"sys-{n}") for n in range(5))
def test_no_row_is_inferred_as_allow(self):
rows = render_rows(self.workloads, [
ReadbackResult("sys-0", "allow", NOW + timedelta(seconds=30)),
ReadbackResult("sys-1", "allow", NOW - timedelta(seconds=1)),
ReadbackResult("sys-2", "deny"),
ReadbackResult("sys-3", "not_applicable"),
], now=NOW)
self.assertEqual(
[row.status for row in rows],
["available", "unchecked", "denied", "not_offered", "unchecked"],
)
self.assertEqual([row.available for row in rows], [True, False, False, False, False])
def test_allow_without_expiry_is_not_available(self):
rows = render_rows(self.workloads[:1], [ReadbackResult("sys-0", "allow")], now=NOW)
self.assertEqual(rows[0].status, "unchecked")
def test_failed_read_renders_every_row_unchecked(self):
rows = render_rows(self.workloads, None, now=NOW)
self.assertEqual({row.status for row in rows}, {"unchecked"})
class _FakeReadback:
def __init__(self, results, systems=None, fail_read=False, fail_systems=False):
self.results = results
self._systems = systems or [_system("informed-decision", "informed-client")]
self.fail_read = fail_read
self.fail_systems = fail_systems
self.reads = []
def systems(self):
if self.fail_systems:
raise ReadbackUnavailable("down")
return tuple(self._systems)
def read(self, actor, systems):
self.reads.append((actor.subject, tuple(systems)))
if self.fail_read:
raise ReadbackUnavailable("down")
return tuple(self.results)
class ReaderTests(unittest.TestCase):
def setUp(self):
self.now = NOW
self.applications = [_bound("informed", "informed-decision", "informed-client")]
def _reader(self, readback):
return ApplicationAccessReader(
readback, issuer=ISSUER, bound_applications=lambda: self.applications,
clock=lambda: self.now,
)
def test_reuses_rows_until_allow_expiry_and_never_past_it(self):
readback = _FakeReadback([
ReadbackResult("informed-decision", "allow", NOW + timedelta(seconds=30), "decision:1"),
])
reader = self._reader(readback)
self.assertEqual(reader.rows(_actor(), "s1")[0].status, "available")
self.now = NOW + timedelta(seconds=20)
self.assertEqual(reader.rows(_actor(), "s1")[0].status, "available")
self.assertEqual(len(readback.reads), 1)
self.now = NOW + timedelta(seconds=30)
reader.rows(_actor(), "s1")
self.assertEqual(len(readback.reads), 2)
def test_cache_is_per_subject_and_dropped_on_forget(self):
readback = _FakeReadback([ReadbackResult("informed-decision", "deny")])
reader = self._reader(readback)
reader.rows(_actor("alice"), "s1")
reader.rows(_actor("bob"), "s1")
self.assertEqual([item[0] for item in readback.reads], ["alice", "bob"])
reader.forget("s1")
reader.rows(_actor("alice"), "s1")
self.assertEqual(len(readback.reads), 3)
def test_read_failure_is_unchecked_and_not_reused(self):
readback = _FakeReadback([], fail_read=True)
reader = self._reader(readback)
self.assertEqual(reader.rows(_actor(), "s1")[0].status, "unchecked")
reader.rows(_actor(), "s1")
self.assertEqual(len(readback.reads), 2)
def test_unreadable_registry_returns_none(self):
reader = self._reader(_FakeReadback([], fail_systems=True))
self.assertIsNone(reader.rows(_actor(), "s1"))
def test_no_bound_workload_makes_no_decision_request(self):
self.applications = []
readback = _FakeReadback([])
self.assertEqual(self._reader(readback).rows(_actor(), "s1"), ())
self.assertEqual(readback.reads, [])
class HTTPAdapterTests(unittest.TestCase):
def setUp(self):
handle, self.token_file = tempfile.mkstemp()
os.write(handle, b"readback-token\n")
os.close(handle)
self.addCleanup(os.unlink, self.token_file)
self.adapter = ApplicationAccessReadbackHTTPAdapter(
base_url="http://readback/", bearer_token_file=self.token_file,
)
def _patch(self, **kwargs):
return patch("user_engine.adapters.application_access.urlopen", **kwargs)
def test_read_sends_check_subject_shape_and_maps_results(self):
body = _Response(json.dumps({
"registry_snapshot_digest": "sha256:" + "0" * 64,
"results": [
{"system": "informed-decision", "effect": "allow", "decision_id": "decision:1",
"lifetime": {"kind": "ttl", "expires_at": "2026-10-10T12:00:30Z"}},
{"system": "markitect", "effect": "not_applicable", "reason": "no rule"},
],
}).encode())
with self._patch(return_value=body) as call:
results = self.adapter.read(_actor(), ["informed-decision", "markitect"])
request = call.call_args.args[0]
self.assertEqual(request.full_url, "http://readback/v1/readback/application_access")
self.assertEqual(request.get_header("Authorization"), "Bearer readback-token")
payload = json.loads(request.data)
self.assertEqual(set(payload), {"tenant", "subject", "systems"})
self.assertEqual(payload["subject"]["tenant"], "tenant-a")
check = _check_payload(_actor())
self.assertEqual(payload["subject"], check["subject"])
self.assertEqual(results[0].effect, "allow")
self.assertEqual(results[0].expires_at, NOW + timedelta(seconds=30))
self.assertEqual(results[1].effect, "not_applicable")
def test_mismatched_or_unknown_results_are_unavailable(self):
bodies = [
{"registry_snapshot_digest": "x", "results": [{"system": "other", "effect": "allow"}]},
{"registry_snapshot_digest": "x", "results": [{"system": "a", "effect": "permit"}]},
{"registry_snapshot_digest": "x", "results": []},
]
for body in bodies:
with self.subTest(body=body), self._patch(return_value=_Response(json.dumps(body).encode())):
with self.assertRaises(ReadbackUnavailable):
self.adapter.read(_actor(), ["a"])
def test_transport_failures_are_unavailable(self):
for error in (URLError("down"), HTTPError("http://readback", 503, "x", {}, None), TimeoutError()):
with self.subTest(error=error), self._patch(side_effect=error):
with self.assertRaises(ReadbackUnavailable):
self.adapter.systems()
def test_empty_token_never_calls(self):
with open(self.token_file, "w", encoding="utf-8"):
pass
with self._patch() as call, self.assertRaises(ReadbackUnavailable):
self.adapter.systems()
call.assert_not_called()
def test_systems_maps_client_binding(self):
body = _Response(json.dumps({
"registry_snapshot_digest": "sha256:" + "0" * 64,
"systems": [
{"id": "informed-decision", "name": "Informed Decision", "readback": True,
"client_binding": {"issuer": ISSUER, "client_id": "informed-client"}},
{"id": "markitect", "readback": False},
],
}).encode())
with self._patch(return_value=body):
systems = self.adapter.systems()
self.assertEqual(systems[0].client_id, "informed-client")
self.assertIsNone(systems[1].client_id)
def test_request_size_is_bounded(self):
with self.assertRaises(ValueError):
self.adapter.read(_actor(), [f"s{n}" for n in range(65)])
def _check_payload(actor):
body = _Response(json.dumps({"id": "decision:1", "effect": "deny"}).encode())
with patch("user_engine.adapters.flex_auth.urlopen", return_value=body) as call:
FlexAuthHTTPAdapter(base_url="http://flex-auth").check(AuthorizationRequest(
actor=actor, resource_type="user-engine:user", resource_id="u",
action="user.read", tenant=actor.tenant, correlation_id="c",
))
return json.loads(call.call_args.args[0].data)
if __name__ == "__main__":
unittest.main()
class PortalApplicationAccessTests(unittest.TestCase):
def setUp(self):
import test_portal_navigation
test_portal_navigation.PortalNavigationTests.setUp(self)
store = self.app.service.store
for app_id, system_id, client_id in [
("informed", "informed-decision", "informed-client"),
("markitect", "markitect", "markitect-client"),
]:
application, binding = _bound(app_id, system_id, client_id)
store.save_application(application)
store.save_binding(binding)
self.readback = _FakeReadback(
[
ReadbackResult("informed-decision", "allow",
datetime.now(UTC) + timedelta(seconds=30), "decision:1"),
ReadbackResult("markitect", "not_applicable"),
],
systems=[_system("informed-decision", "informed-client"),
_system("markitect", "markitect-client")],
)
self.app.application_access = ApplicationAccessReader(
self.readback, issuer=ISSUER,
bound_applications=lambda: [
(store.application(binding.application_id), binding)
for binding in store.all_bindings()
],
)
def _onboarding(self):
from test_web import invoke
return invoke(self.app, "/onboarding", cookie="ue_session=member")[1]
def test_account_page_renders_readback_rows(self):
body = self._onboarding()
self.assertIn(b'<li data-access="available">Informed - Available</li>', body)
self.assertIn(b'<li data-access="not_offered">Markitect - Not available: not offered here</li>', body)
self.assertIn(b"is not a complete inventory", body)
def test_unreadable_registry_says_could_not_be_checked(self):
self.readback.fail_systems = True
self.assertIn(b"Application access could not be checked.", self._onboarding())
def test_logout_drops_the_cached_rows(self):
from test_web import invoke
self._onboarding()
self._onboarding()
self.assertEqual(len(self.readback.reads), 1)
invoke(self.app, "/logout", method="POST", cookie="ue_session=member", form={})
self._onboarding()
self.assertEqual(len(self.readback.reads), 2)
def test_section_absent_without_readback(self):
self.app.application_access = None
self.assertNotIn(b"application-access-list", self._onboarding())