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