feat: report bounded verified access dependency readiness
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
parent
7f0dc78607
commit
c0383c9c2b
9 changed files with 294 additions and 17 deletions
148
tests/test_access_readiness.py
Normal file
148
tests/test_access_readiness.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue