fix: recheck policy decision expiry after audit custody
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 15:56:52 +02:00
parent 72f3513954
commit f11b948e8c
7 changed files with 126 additions and 8 deletions

View file

@ -43,7 +43,7 @@ class Owners:
self.requests.append(request)
if self.policy_down:
raise ConnectionError('private backend details')
return Decision(self.allow, 'decision:1', 'policy:v1')
return Decision(self.allow, 'decision:1', 'policy:v1', time.time()+30)
async def append(self, record):
if self.audit_down:
@ -220,7 +220,7 @@ def test_fact_strings_cannot_be_truthy_grants_and_denials_retain_actor():
with pytest.raises(ValueError):
replace(owners.facts, root_entitled='false')
with pytest.raises(ValueError):
Decision('allow', 'id', 'v1')
Decision('allow', 'id', 'v1', time.time()+30)
owners.actor = replace(owners.actor, subject='ordinary')
with TestClient(runtime(owners)) as client:
assert client.get('/docs', headers=HEADERS).status_code == 403

View file

@ -0,0 +1,76 @@
"""A verified decision must remain usable through audit custody and dispatch."""
import time
from dataclasses import replace
from datetime import timedelta
from types import SimpleNamespace
import pytest
from hub_core.runtime.tables import runtime_messages, runtime_outcome_outbox
from hub_core.security import boundary
from hub_core.security.boundary import Decision
from hub_core.security.policy import verify_decision
from test_access_boundary import HEADERS
from test_access_policy import case, sign
from test_outcome_audit import target, rows, message
@pytest.mark.parametrize('limit', ['decision', 'caller', 'freshness'])
def test_signed_policy_preserves_earliest_verified_deadline(limit):
request,envelope,now = case()
expected = now + timedelta(seconds=30)
if limit == 'decision':
expected = now + timedelta(seconds=1)
envelope['lifetime']['expires_at'] = expected.isoformat()
elif limit == 'caller':
expected = now + timedelta(seconds=2)
envelope['provenance']['caller']['not_after'] = expected.isoformat()
result = verify_decision(envelope,request=request,keys=sign(envelope),
caller='workload:hub',now=now)
assert result.valid_until == expected.timestamp()
@pytest.mark.parametrize('deadline', [None, True, '123', float('nan'), float('inf')])
def test_decision_requires_finite_deadline(deadline):
with pytest.raises(ValueError,match='deadline'):
Decision(True,'decision','policy',deadline)
@pytest.mark.parametrize('elapsed,status', [(0,202), (1,503), (2,503)])
def test_expiry_during_audit_prevents_business_commit(target,monkeypatch,elapsed,status):
client,store,owners,_ = target
now = time.time()
clock = SimpleNamespace(time=lambda: now,monotonic=time.monotonic)
monkeypatch.setattr(boundary,'time',clock)
async def facts(actor,resource):
return replace(owners.facts,checked_at=clock.time())
owners.resolve = facts
evaluate,append = owners.evaluate,owners.append
async def policy(request):
return replace(await evaluate(request),valid_until=now+1)
async def audit(record):
await append(record)
clock.time = lambda: now+elapsed
owners.evaluate,owners.append = policy,audit
response = client.post('/ports/messaging/messages',headers=HEADERS,json=message())
assert response.status_code == status
if status == 503:
assert response.json()['detail'] == 'policy_decision_expired'
assert not rows(store,runtime_messages)
assert not rows(store,runtime_outcome_outbox)
assert [record['outcome'] for record in owners.records] == ['authorized','refused']
assert owners.records[0]['correlation_id'] == owners.records[1]['correlation_id']
else:
assert len(rows(store,runtime_messages)) == len(rows(store,runtime_outcome_outbox)) == 1
def test_already_expired_adapter_decision_is_not_authorized(target):
client,store,owners,_ = target
async def policy(request):
return Decision(True,'expired','policy',1)
owners.evaluate = policy
response = client.post('/ports/messaging/messages',headers=HEADERS,json=message())
assert response.status_code == 503
assert [record['outcome'] for record in owners.records] == ['refused']
assert not rows(store,runtime_messages)
assert not rows(store,runtime_outcome_outbox)