feat: report bounded verified access dependency readiness
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 3s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
tegwick 2026-09-28 14:32:36 +02:00
parent 7f0dc78607
commit c0383c9c2b
9 changed files with 294 additions and 17 deletions

View file

@ -0,0 +1,148 @@
import asyncio
from dataclasses import replace
from unittest.mock import patch
import pytest
from fastapi.testclient import TestClient
from hub_core.security.boundary import ACCESS_DEPENDENCIES
from hub_core.security.identity import AccessFailure
from test_access_boundary import Owners, HEADERS, runtime
async def authorize(controller):
return await controller.authorize('verified-root','hub.test','/docs','request:test','a'*64)
def test_missing_and_expired_observations_are_not_ready():
owners = Owners()
controller = owners.controller()
assert set(controller.readiness_checks().values()) == {'unavailable'}
with patch('hub_core.security.boundary.time.monotonic',return_value=100):
asyncio.run(authorize(controller))
assert set(controller.readiness_checks().values()) == {'ok'}
with patch('hub_core.security.boundary.time.monotonic',return_value=111):
checks = controller.readiness_checks()
assert checks['access_profile'] == 'unavailable'
assert all(checks['access_'+name] == 'stale' for name in ACCESS_DEPENDENCIES)
@pytest.mark.parametrize('dependency',ACCESS_DEPENDENCIES)
def test_owner_failure_is_named_and_recovers(dependency):
owners = Owners()
controller = owners.controller()
method = {'identity':'authenticate','facts':'resolve','policy':'evaluate','audit':'append'}[dependency]
original = getattr(owners,method)
async def failing(*args):
raise RuntimeError('private-credential-or-endpoint')
async def run():
await authorize(controller)
setattr(owners,method,failing)
with pytest.raises((RuntimeError,AccessFailure)):
await authorize(controller)
checks = controller.readiness_checks()
assert checks['access_'+dependency] == 'unavailable'
assert checks['access_profile'] == 'unavailable'
assert 'private' not in str(checks)
setattr(owners,method,original)
await authorize(controller)
assert set(controller.readiness_checks().values()) == {'ok'}
asyncio.run(run())
def test_bad_token_does_not_refresh_or_poison_owner_health():
owners = Owners()
controller = owners.controller()
async def run():
with patch('hub_core.security.boundary.time.monotonic',return_value=100):
await authorize(controller)
with patch('hub_core.security.boundary.time.monotonic',return_value=101):
with pytest.raises(AccessFailure):
await controller.authorize('invalid','hub.test','/docs','r','a'*64)
assert controller.readiness_checks()['access_identity'] == 'ok'
with patch('hub_core.security.boundary.time.monotonic',return_value=111):
assert controller.readiness_checks()['access_identity'] == 'stale'
asyncio.run(run())
def test_verified_denial_is_healthy_policy_but_never_a_grant():
owners = Owners()
owners.allow = False
controller = owners.controller()
async def run():
with pytest.raises(AccessFailure,match='policy_denied'):
await authorize(controller)
assert set(controller.readiness_checks().values()) == {'ok'}
with pytest.raises(AccessFailure,match='policy_denied'):
await authorize(controller) # Healthy observations never authorize.
asyncio.run(run())
def test_untrusted_facts_are_not_a_successful_health_observation():
owners = Owners()
owners.facts = replace(owners.facts,subject='wrong-principal')
controller = owners.controller()
with pytest.raises(AccessFailure,match='untrusted_or_stale_facts'):
asyncio.run(authorize(controller))
assert controller.readiness_checks()['access_facts'] == 'unavailable'
def test_cancelled_dependency_is_unavailable():
owners = Owners()
controller = owners.controller()
started = asyncio.Event()
async def wait_forever(*args):
started.set()
await asyncio.Event().wait()
owners.resolve = wait_forever
async def run():
task = asyncio.create_task(authorize(controller))
await started.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert controller.readiness_checks()['access_facts'] == 'unavailable'
asyncio.run(run())
def test_readyz_reports_verified_dependencies_without_extra_probes():
owners = Owners()
app = runtime(owners)
with TestClient(app) as client:
assert client.get('/readyz').status_code == 401
response = client.get('/readyz',headers=HEADERS)
assert response.status_code == 200
checks = response.json()['checks']
assert all(checks['access_'+name] == 'ok' for name in ACCESS_DEPENDENCIES)
assert len(owners.requests) == 1
owners.policy_down = True
assert client.get('/readyz',headers=HEADERS).status_code == 503
assert app.state.access_controller.readiness_checks()['access_policy'] == 'unavailable'
assert client.get('/healthz').json() == {'status':'ok'}
def test_controller_itself_bounds_a_hung_dependency(monkeypatch):
monkeypatch.setattr('hub_core.security.boundary.AUTHORIZATION_TIMEOUT', .01)
owners = Owners()
controller = owners.controller()
async def hang(*args):
await asyncio.Event().wait()
owners.evaluate = hang
async def run():
with pytest.raises(TimeoutError):
await authorize(controller)
assert controller.readiness_checks()['access_policy'] == 'unavailable'
assert not owners.records
asyncio.run(run())
def test_malformed_policy_result_is_not_reported_healthy():
owners = Owners()
controller = owners.controller()
async def invalid(*args):
return {'allowed': True}
owners.evaluate = invalid
with pytest.raises(AccessFailure):
asyncio.run(authorize(controller))
assert controller.readiness_checks()['access_policy'] == 'unavailable'
assert not owners.records

View file

@ -167,13 +167,11 @@ def test_all_native_mutation_families_keep_attributed_outcomes(target):
from hub_core.conformance import ConformanceHarness
client,store,owners,_ = target
owners.facts = replace(owners.facts,producer_addresses=frozenset({'hub:ops-hub'}))
# Composition is frozen when the app is created; run the twelve business
# checks except dependency readiness, which separately reports no dispatcher.
# The harness must recognize the missing dispatcher as a degraded dependency.
harness = ConformanceHarness(client)
client.headers.update(HEADERS)
report = harness.run()
assert all(c.status == 'pass' for c in report.checks if c.check_id != 'C9'), report.to_dict()
assert next(c for c in report.checks if c.check_id == 'C9').status == 'fail'
assert report.passed, report.to_dict()
assert client.get('/readyz').json()['checks']['outcome_delivery'] == 'unavailable'
pending = rows(store,runtime_outcome_outbox)
operations = {r['envelope']['data']['operation'] for r in pending}