A raising parent-session revocation no longer skips broker-grant revocation, and the reverse; the first failure is re-raised after both are attempted, so the attempt is refused rather than reported as success. Regression tests cover each direction and both failing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 903097@bnt-lap001 Assistant-Session: aa320c8f-c3fa-46e7-8541-0b58e1e45d23
168 lines
7.9 KiB
Python
168 lines
7.9 KiB
Python
"""Synthetic custody capabilities only; no secret value exists anywhere in these tests."""
|
|
from dataclasses import asdict, replace
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
from warden.case_run import Invocation, digest
|
|
from warden.case_receipt_state import Outcome, Pins, Refused
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).parent
|
|
spec = importlib.util.spec_from_file_location('mint_secret_adapter', ROOT / 'mint-secret-adapter.py')
|
|
a = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(a)
|
|
PATH = 'platform/workloads/keycape/forgejo'
|
|
|
|
|
|
def receipt(op, version=3, **over):
|
|
r = {'schema': a.RECEIPT_SCHEMA, 'path': PATH, 'operation': op, 'version': version,
|
|
'created': op == 'mint', 'values_emitted': False}
|
|
r.update(over)
|
|
return r
|
|
|
|
|
|
class MintSecretTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.case = yaml.safe_dump({'spec': {'type': 'oidc-browser-relying-party@1.2', 'parameters':
|
|
{'client_id': 'forgejo', 'client_type': 'confidential'}}}).encode()
|
|
desc = json.dumps(asdict(a.descriptor()), sort_keys=True, separators=(',', ':')).encode()
|
|
pins = Pins(digest(self.case), digest(b'type'), digest(desc), digest(b'catalog'),
|
|
digest(b'case-acceptance:test'), digest(b'run sheet'))
|
|
self.admitted = Invocation('mint-secret', 'railiance-platform', 'forgejo', 'human',
|
|
'founder', 'keycape-client-secret-generate', pins)
|
|
self.run = replace(self.admitted, attempt_id='attempt-one', operation='run')
|
|
self.back = replace(self.admitted, attempt_id='attempt-one', operation='rollback')
|
|
self.calls, self.evidence, self.revoked = [], {}, []
|
|
self.mint_receipt = receipt('mint')
|
|
self.kw = dict(admitted=self.admitted, case_source=self.case,
|
|
case_acceptance='case-acceptance:test', mint=self.mint, destroy=self.destroy,
|
|
publish=self.publish, authenticate=lambda ref, p: self.evidence.get(ref) == p,
|
|
revoke_session=lambda inv: self.revoke('session', inv),
|
|
revoke_grant=lambda inv: self.revoke('grant', inv))
|
|
|
|
def mint(self, path):
|
|
self.calls.append(('mint', path))
|
|
return self.mint_receipt
|
|
|
|
def destroy(self, path, version):
|
|
self.calls.append(('destroy', path, version))
|
|
return receipt('destroy', version)
|
|
|
|
def publish(self, payload):
|
|
ref = 'owner-evidence:' + str(len(self.evidence))
|
|
self.evidence[ref] = payload
|
|
return ref
|
|
|
|
def revoke(self, kind, inv):
|
|
self.revoked.append((kind, inv.operation))
|
|
return kind + '-revoked:' + inv.attempt_id
|
|
|
|
def test_descriptor_is_mint_with_both_revocations(self):
|
|
d = a.descriptor()
|
|
self.assertEqual((d.effect, d.revocation_scopes, d.retry_safe),
|
|
('mint', ('parent-session', 'broker-grant'), False))
|
|
|
|
def test_mint_records_value_free_evidence_and_verifies(self):
|
|
adapter = a.build(**self.kw)
|
|
result = adapter.invoke(self.run)
|
|
self.assertEqual((result.outcome, result.created), (Outcome.SUCCESS, a.CREATED))
|
|
self.assertTrue(adapter.verify(self.run, result))
|
|
payload = json.loads(self.evidence[result.evidence])
|
|
self.assertEqual((payload['path'], payload['version'], payload['values_emitted']), (PATH, 3, False))
|
|
self.assertEqual(self.revoked, [('session', 'run'), ('grant', 'run')])
|
|
self.assertNotIn('value', json.dumps(payload).replace('values_emitted', ''))
|
|
|
|
def test_created_only_rollback_destroys_exact_version(self):
|
|
adapter = a.build(**self.kw)
|
|
adapter.invoke(self.run)
|
|
result = adapter.rollback(self.back, a.CREATED)
|
|
self.assertEqual(result.outcome, Outcome.ROLLED_BACK)
|
|
self.assertEqual(self.calls[-1], ('destroy', PATH, 3))
|
|
with self.assertRaises(Refused): # second rollback has nothing case-created
|
|
adapter.rollback(replace(self.back, attempt_id='attempt-two'), a.CREATED)
|
|
|
|
def test_rollback_without_creation_refused_and_revokes(self):
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Refused):
|
|
adapter.rollback(self.back, a.CREATED)
|
|
self.assertEqual([c[0] for c in self.calls], [])
|
|
self.assertEqual(self.revoked, [('session', 'rollback'), ('grant', 'rollback')])
|
|
|
|
def _failing(self, kind):
|
|
def revoke(inv):
|
|
self.revoked.append((kind, inv.operation))
|
|
raise RuntimeError(kind + ' revocation unavailable')
|
|
return revoke
|
|
|
|
def test_failing_session_revocation_still_revokes_grant(self):
|
|
self.kw['revoke_session'] = self._failing('session')
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Exception):
|
|
adapter.invoke(self.run)
|
|
self.assertEqual(self.revoked, [('session', 'run'), ('grant', 'run')])
|
|
self.assertEqual(self.evidence, {}) # never published as success
|
|
|
|
def test_failing_grant_revocation_still_revokes_session(self):
|
|
self.kw['revoke_grant'] = self._failing('grant')
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Exception):
|
|
adapter.invoke(self.run)
|
|
self.assertEqual(self.revoked, [('session', 'run'), ('grant', 'run')])
|
|
self.assertEqual(self.evidence, {})
|
|
|
|
def test_both_revocations_failing_attempts_both_and_refuses(self):
|
|
self.kw['revoke_session'] = self._failing('session')
|
|
self.kw['revoke_grant'] = self._failing('grant')
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Exception):
|
|
adapter.rollback(self.back, a.CREATED)
|
|
self.assertEqual(self.revoked, [('session', 'rollback'), ('grant', 'rollback')])
|
|
self.assertEqual(self.evidence, {})
|
|
|
|
def test_receipt_with_value_or_extra_field_refused(self):
|
|
for bad in (receipt('mint', value='x'), receipt('mint', values_emitted=True),
|
|
receipt('mint', path='platform/workloads/keycape/other'),
|
|
receipt('mint', created=False), receipt('mint', version='3'), 'opaque'):
|
|
self.mint_receipt = bad
|
|
with self.assertRaises(Refused):
|
|
a.build(**self.kw).invoke(self.run)
|
|
self.assertEqual(self.evidence, {})
|
|
|
|
def test_public_client_and_target_mismatch_refused(self):
|
|
for params in ({'client_id': 'forgejo', 'client_type': 'public'},
|
|
{'client_id': 'other', 'client_type': 'confidential'}):
|
|
case = yaml.safe_dump({'spec': {'parameters': params}}).encode()
|
|
pins = replace(self.admitted.pins, case=digest(case))
|
|
with self.assertRaises(Refused):
|
|
a.build(**dict(self.kw, case_source=case, admitted=replace(self.admitted, pins=pins)))
|
|
|
|
def test_binding_refusals(self):
|
|
cases = [dict(case_source=self.case + b'#'), dict(case_acceptance='other'),
|
|
dict(admitted=replace(self.admitted, lane='key-cape-oidc-login')),
|
|
dict(admitted=replace(self.admitted, pins=replace(self.admitted.pins, adapter=digest(b'x')))),
|
|
dict(admitted=self.run), dict(mint='bao kv put')]
|
|
for over in cases:
|
|
with self.assertRaises(Refused):
|
|
a.build(**dict(self.kw, **over))
|
|
|
|
def test_attempt_binding_and_reused_evidence_refused(self):
|
|
adapter = a.build(**self.kw)
|
|
with self.assertRaises(Refused):
|
|
adapter.invoke(replace(self.run, target='other'))
|
|
self.kw['publish'] = lambda payload: 'owner-evidence:fixed'
|
|
adapter = a.build(**self.kw)
|
|
adapter.invoke(self.run)
|
|
with self.assertRaises(Refused):
|
|
adapter.invoke(replace(self.run, attempt_id='attempt-two'))
|
|
|
|
def test_verify_rejects_foreign_result(self):
|
|
adapter = a.build(**self.kw)
|
|
result = adapter.invoke(self.run)
|
|
self.assertFalse(adapter.verify(replace(self.run, attempt_id='attempt-two'), result))
|
|
self.assertFalse(a.build(**self.kw).verify(self.run, result))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|