Founder live go caafe4be. Config resourceVersion 71471711 -> 71655453 with only this client added; KeyCape restarted on the kept image 6f79a2af. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 132360@bnt-lap001 Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
214 lines
9.9 KiB
Python
214 lines
9.9 KiB
Python
import base64
|
|
import contextlib
|
|
import copy
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
import yaml
|
|
|
|
spec = importlib.util.spec_from_file_location(
|
|
'se_openbao_rollout', Path(__file__).with_name('secrets-engine-openbao-client-rollout.py'))
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
UID = '00000000-0000-4000-8000-000000000001'
|
|
KEYCAPE_EXAMPLE = Path('/home/worsch/key-cape/config/service-clients.example.yaml')
|
|
EXISTING = [
|
|
{'clientId': 'openbao-admin', 'clientType': 'public',
|
|
'redirectUris': ['http://localhost:8250/oidc/callback']},
|
|
{'clientId': 'secrets-engine-approval', 'audience': 'approval-engine',
|
|
'grantTypes': ['client_credentials'], 'clientType': 'confidential',
|
|
'secretRef': 'env:KEYCAPE_SECRETS_ENGINE_APPROVAL_CLIENT_SECRET',
|
|
'serviceSubject': 'service:secrets-engine', 'tenant': 'tenant:platform',
|
|
'roles': ['secrets-engine'], 'tokenLifetime': '15m'},
|
|
]
|
|
|
|
|
|
def fixture(clients=None, tail='tokenLifetime: 15m\npolicyPath: /var/lib/keycape-policy/policy.yaml\n'):
|
|
# Synthetic marker values only; no live Secret is loaded by this suite.
|
|
raw = ('# preserve this comment\nissuer: https://kc.coulomb.social\n'
|
|
'authelia:\n issuer: https://auth.coulomb.social\n clientSecret: synthetic-marker\n'
|
|
'clients:\n' + yaml.safe_dump(json.loads(json.dumps(EXISTING if clients is None else clients)),
|
|
sort_keys=False) + tail)
|
|
secret = {'metadata': {'uid': UID, 'resourceVersion': '7'},
|
|
'data': {'config.yaml': base64.b64encode(raw.encode()).decode(),
|
|
'signing-key.pem': 'c3ludGhldGljLXNpZ25pbmcta2V5'}}
|
|
return secret, raw
|
|
|
|
|
|
def decode(encoded):
|
|
return base64.b64decode(encoded).decode()
|
|
|
|
|
|
class RegistrationTests(unittest.TestCase):
|
|
def test_add_appends_only_this_client_and_preserves_bytes(self):
|
|
secret, raw = fixture()
|
|
before = copy.deepcopy(secret)
|
|
encoded, changed = module.replacement(secret)
|
|
updated = decode(encoded)
|
|
self.assertTrue(changed)
|
|
head = raw[:raw.index('tokenLifetime:')]
|
|
self.assertTrue(updated.startswith(head))
|
|
self.assertTrue(updated.endswith(raw[len(head):]))
|
|
expected = yaml.safe_load(raw)
|
|
expected['clients'].append(module.CLIENT)
|
|
self.assertEqual(yaml.safe_load(updated), expected)
|
|
self.assertEqual(secret, before) # input, including the signing key, untouched
|
|
|
|
def test_add_at_end_of_document(self):
|
|
secret, raw = fixture(tail='')
|
|
updated = decode(module.replacement(secret)[0])
|
|
self.assertTrue(updated.startswith(raw))
|
|
self.assertEqual(yaml.safe_load(updated)['clients'][-1], module.CLIENT)
|
|
|
|
def test_identical_registration_is_noop(self):
|
|
secret, _ = fixture()
|
|
secret['data']['config.yaml'] = module.replacement(secret)[0]
|
|
self.assertEqual(module.replacement(secret), (secret['data']['config.yaml'], False))
|
|
|
|
def test_drift_fails_closed(self):
|
|
for change in [dict(allowedScopes=['openbao:login', 'openbao:admin']),
|
|
dict(audience='openbao'), dict(tokenLifetime='1h'),
|
|
dict(roles=['secrets-engine', 'platform-admin']),
|
|
dict(tenant='tenant:other'), dict(clientType='public'),
|
|
dict(serviceSubject='service:other')]:
|
|
with self.subTest(change=change), self.assertRaises(module.Refused):
|
|
module.replacement(fixture(EXISTING + [dict(module.CLIENT, **change)])[0])
|
|
|
|
def test_duplicates_fail_closed(self):
|
|
for clients in [EXISTING + [module.CLIENT, module.CLIENT],
|
|
EXISTING + [EXISTING[0]]]:
|
|
with self.assertRaises(module.Refused):
|
|
module.replacement(fixture(clients)[0])
|
|
with self.assertRaises(module.Refused):
|
|
module.removal(fixture(clients)[0])
|
|
|
|
def test_secret_ref_reused_by_another_client_fails_closed(self):
|
|
other = dict(EXISTING[1], clientId='other', secretRef=module.CLIENT['secretRef'])
|
|
with self.assertRaises(module.Refused):
|
|
module.replacement(fixture(EXISTING + [other])[0])
|
|
|
|
def test_unverified_upstream_issuer_refused(self):
|
|
secret, raw = fixture()
|
|
raw = raw.replace('https://auth.coulomb.social', 'https://auth.example.invalid')
|
|
secret['data']['config.yaml'] = base64.b64encode(raw.encode()).decode()
|
|
with self.assertRaises(module.Refused):
|
|
module.replacement(secret)
|
|
|
|
def test_rollback_removes_only_this_client_and_restores_exact_bytes(self):
|
|
for tail in ['tokenLifetime: 15m\n', '']:
|
|
secret, raw = fixture(tail=tail)
|
|
secret['data']['config.yaml'] = module.replacement(secret)[0]
|
|
encoded, changed = module.removal(secret)
|
|
self.assertTrue(changed)
|
|
self.assertEqual(decode(encoded), raw)
|
|
|
|
def test_rollback_of_middle_entry_preserves_neighbours(self):
|
|
secret, raw = fixture(EXISTING[:1] + [module.CLIENT] + EXISTING[1:])
|
|
updated = decode(module.removal(secret)[0])
|
|
self.assertEqual(updated, fixture()[1])
|
|
|
|
def test_rollback_absent_is_noop_and_drift_refused(self):
|
|
secret, _ = fixture()
|
|
self.assertEqual(module.removal(secret), (secret['data']['config.yaml'], False))
|
|
with self.assertRaises(module.Refused):
|
|
module.removal(fixture(EXISTING + [dict(module.CLIENT, tokenLifetime='1h')])[0])
|
|
|
|
def test_registration_matches_keycape_source(self):
|
|
if not KEYCAPE_EXAMPLE.is_file():
|
|
self.skipTest('key-cape checkout absent')
|
|
source = yaml.safe_load(KEYCAPE_EXAMPLE.read_text())['clients']
|
|
self.assertEqual([c for c in source if c['clientId'] == module.CLIENT_ID], [module.CLIENT])
|
|
|
|
|
|
class DeploymentBindingTests(unittest.TestCase):
|
|
def deployment(self, env):
|
|
return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', 'env': env}]}}}}
|
|
|
|
def test_proposed_patch_carries_exactly_the_one_binding(self):
|
|
patch = yaml.safe_load(Path(__file__).with_name(
|
|
'secrets-engine-openbao-client.proposed.patch.yaml').read_text())
|
|
container = patch['spec']['template']['spec']['containers'][0]
|
|
self.assertEqual(container, {'name': 'keycape', 'env': [module.ENV_BINDING]})
|
|
|
|
def test_deployment_yaml_binds_exactly_once_after_live_go(self):
|
|
# Folded in under live go caafe4be (2026-10-07).
|
|
docs = yaml.safe_load_all(Path(__file__).with_name('deployment.yaml').read_text())
|
|
dep = next(d for d in docs if d and d.get('kind') == 'Deployment')
|
|
self.assertTrue(module.deployment_binding(dep))
|
|
env = dep['spec']['template']['spec']['containers'][0]['env']
|
|
self.assertEqual([e['name'] for e in env].count(module.ENV_NAME), 1)
|
|
|
|
def test_binding_detection_and_drift(self):
|
|
self.assertTrue(module.deployment_binding(self.deployment([module.ENV_BINDING])))
|
|
wrong = {'name': module.ENV_NAME, 'value': 'synthetic'}
|
|
with self.assertRaises(module.Refused):
|
|
module.deployment_binding(self.deployment([wrong]))
|
|
|
|
|
|
class RunModeTests(unittest.TestCase):
|
|
"""Drive run() against a fake kubectl; nothing reaches a cluster."""
|
|
|
|
def setUp(self):
|
|
self.secret, _ = fixture()
|
|
self.calls = []
|
|
self.original = module.lane.kubectl
|
|
self.env = []
|
|
|
|
def fake(arguments, payload=None):
|
|
self.calls.append((arguments, payload))
|
|
if arguments[:2] == ['get', 'namespace']:
|
|
return {'metadata': {'uid': module.lane.CLUSTER_UID}}
|
|
if 'deployment' in arguments:
|
|
return {'spec': {'template': {'spec': {'containers': [{'name': 'keycape', 'env': self.env}]}}}}
|
|
if 'patch' in arguments:
|
|
return {'data': dict(self.secret['data'], **{'config.yaml': payload[-1]['value']})}
|
|
return copy.deepcopy(self.secret)
|
|
module.lane.kubectl = fake
|
|
|
|
def tearDown(self):
|
|
module.lane.kubectl = self.original
|
|
|
|
def main(self, *argv):
|
|
out = io.StringIO()
|
|
with contextlib.redirect_stdout(out):
|
|
code = module.main(list(argv))
|
|
return code, json.loads(out.getvalue())
|
|
|
|
def test_default_is_inspect_and_never_patches(self):
|
|
code, receipt = self.main()
|
|
self.assertEqual(code, 0)
|
|
self.assertEqual(receipt['mode'], 'inspect')
|
|
self.assertTrue(receipt['change_needed'])
|
|
self.assertFalse(any('patch' in a for a, _ in self.calls))
|
|
self.assertNotIn('synthetic', json.dumps(receipt))
|
|
|
|
def test_dry_run_is_server_side(self):
|
|
code, _ = self.main('dry-run', '--expected-uid', UID, '--expected-resource-version', '7')
|
|
self.assertEqual(code, 0)
|
|
patches = [a for a, _ in self.calls if 'patch' in a]
|
|
self.assertEqual(len(patches), 1)
|
|
self.assertIn('--dry-run=server', patches[0])
|
|
|
|
def test_apply_requires_live_go_and_env_binding(self):
|
|
base = ['apply', '--expected-uid', UID, '--expected-resource-version', '7']
|
|
self.assertEqual(self.main(*base), (1, {'status': 'refused', 'reason': 'founder_live_go_required'}))
|
|
self.assertEqual(self.main(*base, '--live-go-decision', module.LIVE_GO_DECISION),
|
|
(1, {'status': 'refused', 'reason': 'deployment_env_binding_required_first'}))
|
|
self.assertFalse(any('patch' in a for a, _ in self.calls))
|
|
|
|
def test_stale_revision_refused_before_patch(self):
|
|
code, receipt = self.main('dry-run', '--expected-uid', UID, '--expected-resource-version', '6')
|
|
self.assertEqual(receipt, {'status': 'refused', 'reason': 'observed_revision_changed'})
|
|
self.assertFalse(any('patch' in a for a, _ in self.calls))
|
|
|
|
def test_unexpected_errors_are_contained(self):
|
|
self.secret['data']['config.yaml'] = 'not base64 !!'
|
|
self.assertEqual(self.main(), (1, {'status': 'failed', 'reason': 'contained_operation_failed'}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|