railiance-platform/tests/test_keycape_secrets_engine_openbao_custody.py

215 lines
10 KiB
Python
Raw Normal View History

"""CCR-2026-0033 custody child: synthetic values and fakes only; no OpenBao, cluster or real file."""
import contextlib
2026-10-06 14:26:42 +02:00
import copy
import io
import json
import os
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
import yaml
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'scripts'))
import keycape_secrets_engine_openbao_custody as child # noqa: E402
DECISION = '00000000-0000-4000-8000-000000000033'
SYNTHETIC = 'synthetic-test-value-' + 'x' * 43
def result(code=0, stdout=b'', stderr=b''):
return SimpleNamespace(returncode=code, stdout=stdout, stderr=stderr)
class FakeBao:
2026-10-06 14:26:42 +02:00
"""KV-v2 metadata/data behaviour for one path; refuses data reads outright."""
def __init__(self, current=None, fail_write=False, metadata=None):
self.current, self.fail_write, self.calls, self.payloads = current, fail_write, [], []
2026-10-06 14:26:42 +02:00
self.metadata = metadata or {'cas_required': False, 'max_versions': 0,
'delete_version_after': '0s', 'custom_metadata': None}
self.metadata_writes = []
def __call__(self, args, *, payload=None, token=None, allow_failure=False):
self.calls.append(list(args))
if args[:2] == ['token', 'lookup']:
return result(stdout=json.dumps({'data': {'policies': ['default', 'platform-admin']}}).encode())
2026-10-06 14:26:42 +02:00
if args[0] == 'read' and args[-1] == child.KV_DATA:
raise AssertionError('data path read attempted')
if args[0] == 'read' and args[-1] == child.KV_METADATA:
if self.current is None:
return result(2, stderr=b'No value found at ' + child.KV_METADATA.encode())
2026-10-06 14:26:42 +02:00
body = dict(self.metadata, current_version=self.current,
versions={str(self.current): {'destroyed': False, 'deletion_time': ''}})
return result(stdout=json.dumps({'data': body}).encode())
if args[0] == 'write' and child.KV_METADATA in args:
self.metadata_writes.append(payload)
self.metadata.update(copy.deepcopy(payload))
return result()
if args[0] == 'write' and child.KV_DATA in args:
self.payloads.append(payload)
if self.fail_write or payload['options']['cas'] != (self.current or 0):
raise child.LaneError('command_failed')
self.current = (self.current or 0) + 1
return result(stdout=json.dumps({'request_id': 'req-1', 'data': {'version': self.current}}).encode())
raise AssertionError('unexpected bao call: ' + ' '.join(args[:2]))
2026-10-06 14:26:42 +02:00
def approve(path, base, name, decision=DECISION):
ccr = yaml.safe_load(path.read_text())
ccr['status'] = 'approved'
ccr['openbao']['auth']['bound_claims_confirmed'] = True
ccr['review']['comments'] = [
{'at': '2026-10-06', 'reviewer': r, 'decision': 'approved',
'comment': 'custody child approved by decision ' + decision} for r in child.APPROVERS]
out = base / name
out.write_text(yaml.safe_dump(ccr, sort_keys=False))
return out, ccr
class CustodyChild(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
2026-10-06 14:26:42 +02:00
self.ccr, data = approve(child.CCR_PATH, base, 'CCR-2026-0033.yaml')
self.desired = data['openbao']['kv_metadata']
self.sibling, _ = approve(child.SIBLING_PATH, base, 'CCR-2026-0034.yaml')
self.bao = FakeBao()
2026-10-06 14:26:42 +02:00
self.patches = [patch.object(child, 'CCR_PATH', self.ccr), patch.object(child, 'SIBLING_PATH', self.sibling),
patch.object(child, 'bao', self.bao), patch.object(child, 'attended_envelope', lambda: True),
patch.object(child.secrets, 'token_urlsafe', lambda n: SYNTHETIC)]
for p in self.patches:
p.start()
self.umask = os.umask(0o022)
def tearDown(self):
os.umask(self.umask)
for p in reversed(self.patches):
p.stop()
self.tmp.cleanup()
def run_child(self, *extra, decision=DECISION):
receipt = Path(self.tmp.name) / ('receipt-%d.json' % len(list(Path(self.tmp.name).glob('receipt-*'))))
out, err = io.StringIO(), io.StringIO()
2026-10-06 14:26:42 +02:00
argv = ['provision', '--receipt', str(receipt), *extra]
if decision is not None:
argv += ['--approval-decision', decision]
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
code = child.main(argv)
text = receipt.read_text()
2026-10-06 14:26:42 +02:00
for blob in (text, out.getvalue(), err.getvalue(), json.dumps(self.bao.calls), ' '.join(argv)):
self.assertNotIn(SYNTHETIC, blob)
2026-10-06 14:26:42 +02:00
self.assertEqual(out.getvalue(), '')
self.assertEqual(oct(receipt.stat().st_mode & 0o777), oct(0o600))
2026-10-06 14:26:42 +02:00
return code, json.loads(text)
def test_refuses_without_or_with_wrong_decision(self):
with self.assertRaises(SystemExit), contextlib.redirect_stderr(io.StringIO()):
child.main(['provision', '--receipt', str(Path(self.tmp.name) / 'r.json')])
for decision, reason in (('not-a-uuid', 'approval_decision_id_required'),
(child.BOUNDARY_DECISION, 'boundary_decision_does_not_approve_custody_child'),
('11111111-1111-4111-8111-111111111111', 'custody_child_decision_not_recorded_in_ccr')):
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child(decision=decision)
self.assertEqual((code, receipt['error']), (1, reason))
self.assertEqual(self.bao.calls, [])
def proposed_copy(self, name):
text = (ROOT / 'credential-change-requests' / name).read_text()
path = Path(self.tmp.name) / name
path.write_text(text.replace('\nstatus: approved\n', '\nstatus: proposed\n', 1))
return path
def test_proposed_ccrs_are_refused(self):
with patch.object(child, 'CCR_PATH', self.proposed_copy('CCR-2026-0033.yaml')):
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child()
self.assertEqual((code, receipt['error']), (1, 'approved_ccr_required'))
with patch.object(child, 'SIBLING_PATH', self.proposed_copy('CCR-2026-0034.yaml')):
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child()
self.assertEqual((code, receipt['error']), (1, 'approved_ccr_required'))
self.assertEqual(self.bao.calls, [])
2026-10-06 14:26:42 +02:00
def test_refuses_ambient_token_or_outside_envelope(self):
with patch.object(child, 'attended_envelope', lambda: False):
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child()
self.assertEqual(receipt['error'], 'attended_warden_envelope_required')
self.assertEqual(self.bao.calls, [])
2026-10-06 14:26:42 +02:00
self.patches[3].stop()
try:
with patch.object(child.Path, 'home', lambda: Path('/x/.warden-attended-login/u')):
with patch.dict(os.environ, {}, clear=False):
os.environ.pop('BAO_TOKEN', None)
os.environ.pop('VAULT_TOKEN', None)
self.assertTrue(child.attended_envelope())
for var in ('BAO_TOKEN', 'VAULT_TOKEN'):
with patch.dict(os.environ, {var: 'synthetic-ambient'}):
self.assertFalse(child.attended_envelope())
code, receipt = self.run_child()
self.assertEqual(receipt['error'], 'attended_warden_envelope_required')
finally:
self.patches[3].start()
self.assertEqual(self.bao.calls, [])
2026-10-06 14:26:42 +02:00
def test_create_writes_kv_cas0_and_metadata(self):
code, receipt = self.run_child()
self.assertEqual(code, 0)
self.assertEqual(receipt['status'], 'custody_written_pending_issuer_delivery')
2026-10-06 14:26:42 +02:00
self.assertEqual((receipt['kv_version'], receipt['kv_cas'], receipt['kv_metadata']), (1, 0, 'applied'))
self.assertEqual(self.bao.payloads, [{'options': {'cas': 0}, 'data': {'CLIENT_SECRET': SYNTHETIC}}])
2026-10-06 14:26:42 +02:00
self.assertEqual(self.bao.metadata_writes, [self.desired])
self.assertTrue(self.bao.metadata['cas_required'])
self.assertEqual(self.bao.metadata['max_versions'], 2)
self.assertEqual(self.bao.metadata['custom_metadata']['ccr'], 'CCR-2026-0033')
self.assertEqual(sorted(p.name for p in Path(self.tmp.name).iterdir()),
['CCR-2026-0033.yaml', 'CCR-2026-0034.yaml', 'receipt-0.json'])
def test_existing_entry_requires_rotate_and_rotation_uses_current_cas(self):
self.bao.current = 1
2026-10-06 14:26:42 +02:00
self.bao.metadata.update(copy.deepcopy(self.desired))
code, receipt = self.run_child()
self.assertEqual((code, receipt['error']), (1, 'existing_custody_requires_rotate'))
self.assertEqual(self.bao.payloads, [])
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child('--rotate')
self.assertEqual((code, receipt['kv_cas'], receipt['kv_version'], receipt['kv_metadata']), (0, 1, 2, 'unchanged'))
self.assertEqual(self.bao.metadata_writes, [])
2026-10-06 14:26:42 +02:00
def test_rotate_without_entry_is_refused(self):
code, receipt = self.run_child('--rotate')
self.assertEqual((code, receipt['error']), (1, 'rotation_requires_existing_custody'))
self.assertEqual(self.bao.payloads, [])
2026-10-06 14:26:42 +02:00
def test_failed_kv_write_records_no_version_and_no_metadata(self):
self.bao.fail_write = True
2026-10-06 14:26:42 +02:00
code, receipt = self.run_child()
self.assertEqual((code, receipt['kv_written'], receipt['error']), (1, False, 'command_failed'))
self.assertEqual(self.bao.metadata_writes, [])
def test_metadata_readback_mismatch_fails_but_records_written_version(self):
real = self.bao.__call__
bao = self.bao
def wrapper(args, **kw):
if args[0] == 'write' and child.KV_METADATA in args:
bao.calls.append(list(args))
return result()
return real(args, **kw)
with patch.object(child, 'bao', wrapper):
code, receipt = self.run_child()
self.assertEqual((code, receipt['kv_written'], receipt['kv_version'], receipt['error']),
(1, True, 1, 'kv_metadata_readback_mismatch'))
def test_receipt_contains_only_metadata_keys(self):
2026-10-06 14:26:42 +02:00
_, receipt = self.run_child()
self.assertTrue(set(receipt) <= {
'schema', 'ccr', 'status', 'phase', 'started_at', 'finished_at', 'credential_values_emitted',
2026-10-06 14:26:42 +02:00
'client_side_read_admitted', 'kv_written', 'approval_decision_id', 'ccr_sha256',
'kv_version', 'kv_request_id', 'kv_cas', 'kv_metadata', 'error'})
self.assertFalse(any('consumer' in key for key in receipt))
if __name__ == '__main__':
unittest.main()