net-kingdom/sso-mfa/k8s/keycape/test_mint_secret_adapter.py
tegwick eeed8e118e NK48-R2-F1: revoke session and broker grant independently in mint-secret
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
2026-10-17 11:07:16 +02:00

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()