Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
250 lines
10 KiB
Python
250 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import time
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from hub_core.runtime.app import create_app
|
|
from hub_core.runtime.config import RuntimeSettings
|
|
from hub_core.runtime.store import InMemoryPortStore
|
|
from hub_core.security.boundary import (
|
|
AccessController, Actor, Decision, LiveFacts, iter_routes, route_key,
|
|
)
|
|
from hub_core.security.identity import AccessFailure
|
|
|
|
|
|
class Owners:
|
|
def __init__(self):
|
|
self.actor = Actor('https://issuer.example', 'immutable-root', 'tenant:platform',
|
|
'human', 'aal2', int(time.time()), int(time.time()) + 300)
|
|
self.facts = LiveFacts(self.actor.issuer, self.actor.subject, self.actor.tenant,
|
|
'tenant:platform', True, True, True, True, time.time(),
|
|
'owner-receipt', frozenset({'agent:root'}))
|
|
self.records, self.requests = [], []
|
|
self.allow = True
|
|
self.audit_down = False
|
|
self.policy_down = False
|
|
|
|
async def authenticate(self, token):
|
|
if token != 'verified-root':
|
|
raise AccessFailure(401, 'invalid_access_token')
|
|
return self.actor
|
|
|
|
async def resolve(self, actor, resource):
|
|
return self.facts
|
|
|
|
async def evaluate(self, request):
|
|
self.requests.append(request)
|
|
if self.policy_down:
|
|
raise ConnectionError('private backend details')
|
|
return Decision(self.allow, 'decision:1', 'policy:v1')
|
|
|
|
async def append(self, record):
|
|
if self.audit_down:
|
|
raise ConnectionError('private audit details')
|
|
self.records.append(record)
|
|
|
|
def controller(self):
|
|
return AccessController(identity=self, facts=self, policy=self, audit=self,
|
|
root_issuer='https://issuer.example', root_subject='immutable-root')
|
|
|
|
|
|
def runtime(owners=None):
|
|
return create_app(settings=RuntimeSettings(environment='test', access_mode='enforce'),
|
|
port_store=InMemoryPortStore(),
|
|
access_controller=owners.controller() if owners else None)
|
|
|
|
|
|
HEADERS = {'Authorization': 'Bearer verified-root'}
|
|
CATALOG = json.loads(Path('hub_core/security/routes.json').read_text())['routes']
|
|
SURFACES = [(key.split(':', 2)[0], key.split(':', 2)[1]) for key in CATALOG]
|
|
|
|
|
|
@pytest.mark.parametrize('method,path', SURFACES)
|
|
def test_every_catalog_surface_denies_anonymous(method, path):
|
|
with TestClient(runtime()) as client:
|
|
response = client.request(method, path)
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_production_is_closed_without_owner_adapters():
|
|
app = create_app(settings=RuntimeSettings(environment='production'))
|
|
with TestClient(app) as client:
|
|
assert client.get('/healthz').json() == {'status': 'ok'}
|
|
assert client.get('/readyz').status_code == 401
|
|
assert client.get('/ports/projections/hub_registry', headers=HEADERS).status_code == 503
|
|
assert client.get('/healthz/').status_code == 401
|
|
with pytest.raises(ValueError):
|
|
RuntimeSettings(environment='production', access_mode='development')
|
|
|
|
|
|
def test_root_access_requires_fresh_facts_and_audit_on_every_request():
|
|
owners = Owners()
|
|
with TestClient(runtime(owners)) as client:
|
|
first = client.get('/ports/projections/hub_registry', headers=HEADERS)
|
|
assert first.status_code == 200
|
|
assert first.headers['cache-control'] == 'no-store'
|
|
assert owners.requests[0].facts.root_entitled
|
|
owners.facts = replace(owners.facts, root_entitled=False)
|
|
assert client.get('/ports/projections/hub_registry', headers=HEADERS).status_code == 403
|
|
assert len(owners.requests) == 1
|
|
assert owners.records[0]['outcome'] == 'authorized'
|
|
|
|
|
|
@pytest.mark.parametrize('change,status', [
|
|
({'subject': 'ordinary'}, 403), ({'issuer': 'https://other.example'}, 403),
|
|
({'assurance': 'aal1'}, 403), ({'tenant': 'tenant:other'}, 403),
|
|
({'expires_at': 1}, 401),
|
|
])
|
|
def test_root_cannot_be_claimed_by_name_or_role(change, status):
|
|
owners = Owners()
|
|
owners.actor = replace(owners.actor, **change)
|
|
with TestClient(runtime(owners)) as client:
|
|
assert client.get('/docs', headers=HEADERS).status_code == status
|
|
|
|
|
|
@pytest.mark.parametrize('change,status', [
|
|
({'checked_at': 1}, 503), ({'subject': 'different'}, 503),
|
|
({'account_active': False}, 403), ({'actor_tenant_active': False}, 403),
|
|
({'target_tenant_active': False}, 403), ({'target_tenant': 'tenant:other'}, 403),
|
|
])
|
|
def test_authoritative_account_and_tenant_checks(change, status):
|
|
owners = Owners()
|
|
owners.facts = replace(owners.facts, **change)
|
|
with TestClient(runtime(owners)) as client:
|
|
assert client.get('/openapi.json', headers=HEADERS).status_code == status
|
|
|
|
|
|
@pytest.mark.parametrize('attribute,status', [('allow', 403), ('policy_down', 503), ('audit_down', 503)])
|
|
def test_denial_and_dependency_failure_never_reach_handler(attribute, status):
|
|
owners = Owners()
|
|
setattr(owners, attribute, attribute != 'allow')
|
|
with TestClient(runtime(owners)) as client:
|
|
result = client.post('/ports/messaging/messages', headers=HEADERS, json={})
|
|
assert result.status_code == status
|
|
assert 'private' not in result.text
|
|
|
|
|
|
def test_new_route_and_wrong_method_remain_denied():
|
|
owners = Owners()
|
|
app = runtime(owners)
|
|
calls = []
|
|
|
|
@app.get('/newly-added')
|
|
def new_route():
|
|
calls.append(True)
|
|
|
|
with TestClient(app) as client:
|
|
for path in ['/newly-added', '/unknown', '/ports/projections/hub_registry/']:
|
|
assert client.get(path, headers=HEADERS).status_code == 403
|
|
assert client.delete('/docs', headers=HEADERS).status_code == 403
|
|
assert calls == []
|
|
|
|
|
|
def test_native_sender_is_bound_and_body_reaches_handler():
|
|
owners = Owners()
|
|
body = {'schema_version': '0.1.0', 'correlation_id': 'f7cffcab-4c02-419e-89e5-0b463f5b433a',
|
|
'from_address': 'agent:root', 'to_addresses': ['agent:reader'], 'body': 'private text'}
|
|
with TestClient(runtime(owners)) as client:
|
|
assert client.post('/ports/messaging/messages', headers=HEADERS, json=body).status_code == 202
|
|
body['from_address'] = 'agent:someone-else'
|
|
assert client.post('/ports/messaging/messages', headers=HEADERS, json=body).status_code == 403
|
|
assert 'private text' not in json.dumps(owners.records)
|
|
|
|
|
|
def test_catalog_covers_current_routes_and_does_not_auto_admit():
|
|
app = runtime()
|
|
missing = [route_key(r, method) for r in iter_routes(app) if hasattr(r, 'methods')
|
|
for method in r.methods if r.path != '/healthz' and route_key(r, method) not in CATALOG]
|
|
assert missing == []
|
|
|
|
|
|
def test_concurrent_requests_keep_separate_contexts():
|
|
owners = Owners()
|
|
app = runtime(owners)
|
|
|
|
async def run():
|
|
async with httpx.AsyncClient(transport=httpx.ASGITransport(app), base_url='http://test') as client:
|
|
return await asyncio.gather(*[
|
|
client.get('/ports/projections/hub_registry', headers=HEADERS,
|
|
params={'n': n}) for n in range(10)
|
|
])
|
|
|
|
results = asyncio.run(run())
|
|
assert all(r.status_code == 200 for r in results)
|
|
assert len({r.correlation_id for r in owners.requests}) == 10
|
|
assert len({r.request_digest for r in owners.requests}) == 10
|
|
|
|
|
|
def test_event_provenance_overrides_asserted_producer():
|
|
from datetime import datetime, timezone
|
|
owners = Owners()
|
|
event = {'schema_version': '0.1.0', 'correlation_id': 'f7cffcab-4c02-419e-89e5-0b463f5b433a',
|
|
'event_type': 'hub.progress.recorded', 'occurred_at': datetime.now(timezone.utc).isoformat(),
|
|
'subject_refs': {'hub': 'untrusted-business-reference'},
|
|
'payload': {'_hub_access': {'subject': 'forged'}}}
|
|
with TestClient(runtime(owners)) as client:
|
|
assert client.post('/ports/events/progress', headers=HEADERS, json=event).status_code == 202
|
|
record = client.get('/ports/projections/progress_events', headers=HEADERS).json()
|
|
item = record['data']['items'][0]
|
|
assert item['payload']['_hub_access']['subject'] == 'immutable-root'
|
|
|
|
|
|
def test_embedded_router_uses_the_same_boundary():
|
|
from fastapi import FastAPI
|
|
from hub_core.security.boundary import AccessBoundary
|
|
owners = Owners()
|
|
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
|
|
calls = []
|
|
@app.get('/embedded')
|
|
def embedded():
|
|
calls.append(True)
|
|
return {'ok': True}
|
|
route = next(iter(iter_routes(app)))
|
|
app.add_middleware(AccessBoundary, host=app, controller=owners.controller(),
|
|
catalog={route_key(route, 'GET'): 'extension.read'})
|
|
with TestClient(app) as client:
|
|
assert client.get('/embedded').status_code == 401
|
|
assert client.get('/embedded', headers=HEADERS).status_code == 200
|
|
assert calls == [True]
|
|
|
|
|
|
def test_fact_strings_cannot_be_truthy_grants_and_denials_retain_actor():
|
|
owners = Owners()
|
|
with pytest.raises(ValueError):
|
|
replace(owners.facts, root_entitled='false')
|
|
with pytest.raises(ValueError):
|
|
Decision('allow', 'id', 'v1')
|
|
owners.actor = replace(owners.actor, subject='ordinary')
|
|
with TestClient(runtime(owners)) as client:
|
|
assert client.get('/docs', headers=HEADERS).status_code == 403
|
|
assert owners.records[-1]['subject'] == 'ordinary'
|
|
assert owners.records[-1]['action']
|
|
assert owners.records[-1]['request_digest']
|
|
|
|
|
|
def test_slow_request_body_times_out_before_authority_or_handler():
|
|
from hub_core.security.boundary import AccessBoundary
|
|
owners = Owners()
|
|
host = runtime(owners)
|
|
called = []
|
|
messages = []
|
|
async def handler(scope, receive, send):
|
|
called.append(True)
|
|
boundary = AccessBoundary(handler, host=host, controller=owners.controller(), body_timeout=0.01)
|
|
scope = {'type':'http','method':'POST','path':'/ports/messaging/messages',
|
|
'headers':[(b'authorization',b'Bearer verified-root')], 'query_string':b''}
|
|
async def receive():
|
|
await asyncio.Future()
|
|
async def send(message):
|
|
messages.append(message)
|
|
asyncio.run(boundary(scope,receive,send))
|
|
assert messages[0]['status'] == 408
|
|
assert not called and not owners.requests
|
|
assert owners.records[-1]['reason'] == 'request_body_timeout'
|