131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
|
|
"""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()
|