feat: integrate durable authorization audit and runtime composition
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
This commit is contained in:
parent
3e386147fd
commit
c9b6916dac
14 changed files with 767 additions and 16 deletions
94
tests/test_access_audit.py
Normal file
94
tests/test_access_audit.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from hub_core.security.audit import AuditCoreSink
|
||||
from hub_core.security.identity import AccessFailure
|
||||
|
||||
RECORD = {'profile': 'hub-core.access/1.0.0', 'correlation_id': 'request:123',
|
||||
'outcome': 'authorized', 'subject': 'root-sub', 'actor_tenant': 'tenant:platform',
|
||||
'target_tenant': 'tenant:platform', 'decision_id': 'decision:123'}
|
||||
READY = {'status': 'ok', 'durable': True, 'custody_class': 'operational'}
|
||||
|
||||
|
||||
def run_sink(tmp_path, handler, record=RECORD):
|
||||
credential = tmp_path/'audit-token'
|
||||
credential.write_text('audit-only-fixture')
|
||||
async def run():
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client:
|
||||
sink = AuditCoreSink(base_url='https://audit.example', token_file=credential, client=client)
|
||||
await sink.append(record)
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.parametrize('code,state', [(202, 'accepted'), (200, 'duplicate')])
|
||||
def test_requires_exact_durable_custody_receipt(tmp_path, code, state):
|
||||
requests = []
|
||||
def handle(request):
|
||||
requests.append(request)
|
||||
assert not request.extensions.get('follow_redirects')
|
||||
if request.url.path == '/readyz':
|
||||
assert 'authorization' not in request.headers
|
||||
return httpx.Response(200, json=READY)
|
||||
envelope = json.loads(request.content)
|
||||
assert set(envelope) == {'id','type','source','subject','tenant','correlation_id','occurred_at','data'}
|
||||
assert request.headers['authorization'] == 'Bearer audit-only-fixture'
|
||||
assert request.headers['idempotency-key'] == envelope['id']
|
||||
assert envelope['data'] == RECORD
|
||||
assert envelope['source'] == 'hub-core'
|
||||
assert envelope['tenant'] == 'tenant:platform'
|
||||
return httpx.Response(code, json={'status': state, 'reference': 'audit:'+envelope['id']})
|
||||
run_sink(tmp_path, handle)
|
||||
assert [r.url.path for r in requests] == ['/readyz', '/v1/events']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('code,receipt', [
|
||||
(200, {'status':'ok'}), (202, {'status':'accepted'}),
|
||||
(200, {'status':'accepted','reference':'x'}), (202, {'status':'duplicate','reference':'x'}),
|
||||
(400, {'status':'accepted','reference':'x'}), (401, {}), (403, {}), (409, {}), (503, {}),
|
||||
(307, {}),
|
||||
])
|
||||
def test_unacknowledged_custody_never_allows_execution(tmp_path, code, receipt):
|
||||
def handle(request):
|
||||
if request.url.path == '/readyz':
|
||||
return httpx.Response(200, json=READY)
|
||||
return httpx.Response(code, json=receipt, headers={'Location':'https://untrusted.example'})
|
||||
with pytest.raises(AccessFailure, match='audit_unavailable'):
|
||||
run_sink(tmp_path, handle)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('readiness', [
|
||||
{**READY, 'custody_class':'development'}, {**READY, 'durable':False},
|
||||
{**READY, 'status':'unavailable'}, {**READY, 'durable':'true'}, {},
|
||||
])
|
||||
def test_receiver_fallback_fails_before_post(tmp_path, readiness):
|
||||
def handle(request):
|
||||
assert request.url.path == '/readyz'
|
||||
return httpx.Response(200, json=readiness)
|
||||
with pytest.raises(AccessFailure):
|
||||
run_sink(tmp_path, handle)
|
||||
|
||||
|
||||
def test_rotation_is_read_each_time_and_lost_receipt_denies(tmp_path):
|
||||
token_file = tmp_path/'audit-token'
|
||||
seen = []
|
||||
def handle(request):
|
||||
if request.url.path == '/readyz':
|
||||
return httpx.Response(200, json=READY)
|
||||
seen.append(request.headers['authorization'])
|
||||
if len(seen) == 2:
|
||||
raise httpx.ReadTimeout('sensitive private upstream details')
|
||||
return httpx.Response(202, json={'status':'accepted','reference':'audit:1'})
|
||||
async def run():
|
||||
async with httpx.AsyncClient(transport=httpx.MockTransport(handle)) as client:
|
||||
sink = AuditCoreSink(base_url='https://audit.example', token_file=token_file, client=client)
|
||||
token_file.write_text('first')
|
||||
await sink.append(RECORD)
|
||||
token_file.write_text('replacement')
|
||||
with pytest.raises(AccessFailure) as result:
|
||||
await sink.append(RECORD)
|
||||
assert str(result.value) == 'audit_unavailable'
|
||||
asyncio.run(run())
|
||||
assert seen == ['Bearer first', 'Bearer replacement']
|
||||
Loading…
Add table
Add a link
Reference in a new issue