"""Initial root-token check/revoke on a fake OpenBao (NK-WP-0055-T16). No network, synthetic token.""" import importlib.util import io import json from pathlib import Path import tempfile import unittest import urllib.error ROOT = Path(__file__).resolve().parents[1] _spec = importlib.util.spec_from_file_location('revoke_root', ROOT / 'scripts/openbao_revoke_initial_root_token.py') m = importlib.util.module_from_spec(_spec) _spec.loader.exec_module(m) TOKEN = 's.syntheticRootToken0123' ADDR, BG = 'http://127.0.0.1:18201', 'http://127.0.0.1:18210' DECISION = '11111111-2222-3333-4444-555555555555' class Response(io.BytesIO): def __init__(self, status, body): super().__init__(json.dumps(body).encode() if body is not None else b'') self.status = status def __enter__(self): return self def __exit__(self, *a): self.close() class FakeBao: def __init__(self, policies=('root',), ttl=0, orphan=True, breakglass=True, revoke_code=204): self.valid = True self.policies, self.ttl, self.orphan = list(policies), ttl, orphan self.breakglass, self.revoke_code = breakglass, revoke_code self.calls = [] def open(self, request, timeout=None): url, method = request.full_url, request.get_method() self.calls.append((method, url)) if url == BG + '/v1/sys/generate-root/attempt': if not self.breakglass: raise urllib.error.HTTPError(url, 405, 'disabled', {}, None) return Response(200, {'started': False}) assert url.startswith(ADDR) if request.headers.get('X-vault-token') != TOKEN or not self.valid: raise urllib.error.HTTPError(url, 403, 'denied', {}, None) if url.endswith('/lookup-self'): return Response(200, {'data': {'policies': self.policies, 'ttl': self.ttl, 'orphan': self.orphan, 'expire_time': None, 'id': TOKEN}}) if url.endswith('/revoke-self'): if self.revoke_code == 204: self.valid = False return Response(self.revoke_code, None) raise AssertionError(url) class InitialRootTokenTest(unittest.TestCase): def setUp(self): self.dir = tempfile.TemporaryDirectory() self.receipt = Path(self.dir.name) / 'r.json' def tearDown(self): self.dir.cleanup() def run_mode(self, mode, bao, **kw): return m.run(mode, ADDR, BG, TOKEN, self.receipt, kw.get('decision', DECISION if mode == 'revoke' else None), bao) def test_check_records_facts_without_revoking(self): bao = FakeBao() record = self.run_mode('check', bao) self.assertEqual(record['status'], 'checked') self.assertTrue(bao.valid) self.assertNotIn(('POST', ADDR + '/v1/auth/token/revoke-self'), bao.calls) text = self.receipt.read_text() self.assertNotIn(TOKEN, text) self.assertEqual(oct(self.receipt.stat().st_mode & 0o777), '0o600') def test_revoke_confirms_204_then_403(self): bao = FakeBao() record = self.run_mode('revoke', bao) self.assertEqual(record['status'], 'revoked') self.assertEqual(record['revoke_status'], 204) self.assertEqual(record['after'], {'valid': False, 'lookup_status': 403}) self.assertEqual(record['decision_id'], DECISION) self.assertNotIn(TOKEN, self.receipt.read_text()) def test_revoke_refused_without_breakglass_path(self): bao = FakeBao(breakglass=False) record = self.run_mode('revoke', bao) self.assertEqual(record['status'], 'refused_no_breakglass_path') self.assertTrue(bao.valid) def test_revoke_refused_for_a_non_initial_root_token(self): for kw in ({'policies': ('default', 'platform-admin')}, {'ttl': 3600}, {'orphan': False}): bao = FakeBao(**kw) self.receipt.unlink(missing_ok=True) record = self.run_mode('revoke', bao) self.assertEqual(record['status'], 'refused_not_initial_root_token') self.assertTrue(bao.valid) def test_already_invalid_token_is_reported(self): bao = FakeBao() bao.valid = False self.assertEqual(self.run_mode('check', bao)['status'], 'token_already_invalid') def test_unconfirmed_revoke_is_not_reported_as_revoked(self): self.assertEqual(self.run_mode('revoke', FakeBao(revoke_code=500))['status'], 'revoke_unconfirmed') def test_refusals_before_any_call(self): bao = FakeBao() cases = [ lambda: m.run('revoke', ADDR, BG, TOKEN, self.receipt, None, bao), lambda: m.run('revoke', ADDR, BG, TOKEN, self.receipt, 'not-a-decision', bao), lambda: m.run('check', 'http://10.0.0.1:8200', BG, TOKEN, self.receipt, None, bao), lambda: m.run('check', 'https://127.0.0.1:8200', BG, TOKEN, self.receipt, None, bao), lambda: m.run('check', ADDR, BG, 'bad token\n', self.receipt, None, bao), ] for case in cases: with self.assertRaises(m.Refused): case() self.receipt.write_text('{}') with self.assertRaises(m.Refused): m.run('check', ADDR, BG, TOKEN, self.receipt, None, bao) self.assertEqual(bao.calls, []) if __name__ == '__main__': unittest.main()