131 lines
6 KiB
Python
131 lines
6 KiB
Python
|
|
"""Offline tests for the operators/nextcloud retirement helper against a fake ``bao``."""
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
import textwrap
|
||
|
|
import unittest
|
||
|
|
|
||
|
|
ROOT = Path(__file__).resolve().parents[1]
|
||
|
|
SCRIPT = ROOT / 'scripts/operators_entry_retire.py'
|
||
|
|
CONFIRM = 'RETIRE operators/nextcloud'
|
||
|
|
ONE_SHOT = 'hvs.oneshot-' + 'K3l4' * 6
|
||
|
|
|
||
|
|
# KV calls are refused unless they carry the one-shot token and its policy exists.
|
||
|
|
FAKE_BAO = textwrap.dedent(f'''\
|
||
|
|
#!/usr/bin/env python3
|
||
|
|
import json, os, sys
|
||
|
|
sf = os.environ['FAKE_BAO_STATE']
|
||
|
|
st = json.load(open(sf))
|
||
|
|
a = sys.argv[1:]
|
||
|
|
tok = os.environ.get('BAO_TOKEN', 'caller')
|
||
|
|
st['calls'].append({{'args': a, 'token': tok}})
|
||
|
|
def save(): json.dump(st, open(sf, 'w'))
|
||
|
|
def out(obj): save(); print(json.dumps(obj)); sys.exit(0)
|
||
|
|
def deny(): save(); sys.stderr.write('permission denied\\n'); sys.exit(2)
|
||
|
|
if a[:2] == ['token', 'lookup']: out({{'data': {{'policies': st['caller_policies']}}}})
|
||
|
|
if a[:2] == ['policy', 'write']: st['policy'] = open(a[3]).read(); save(); sys.exit(0)
|
||
|
|
if a[:2] == ['policy', 'delete']: st.pop('policy', None); save(); sys.exit(0)
|
||
|
|
if a[:2] == ['token', 'create']:
|
||
|
|
if st.get('fail_create'): save(); sys.exit(2)
|
||
|
|
out({{'auth': {{'client_token': '{ONE_SHOT}'}}}})
|
||
|
|
if a[:2] == ['token', 'revoke']: st['revoked'] = tok; save(); sys.exit(0)
|
||
|
|
if tok != '{ONE_SHOT}' or 'policy' not in st: deny()
|
||
|
|
if a[0] == 'read':
|
||
|
|
rest = a[-1].split('/', 2)[2]
|
||
|
|
if rest not in st['kv']: save(); sys.stderr.write('No value found\\n'); sys.exit(2)
|
||
|
|
n = st['kv'][rest]
|
||
|
|
out({{'data': {{'current_version': n, 'versions': {{str(i + 1): {{}} for i in range(n)}}}}}})
|
||
|
|
if a[0] == 'delete':
|
||
|
|
if st.get('fail_delete'): deny()
|
||
|
|
st['kv'].pop(a[1].split('/', 2)[2], None); save(); sys.exit(0)
|
||
|
|
save(); sys.exit(9)
|
||
|
|
''')
|
||
|
|
|
||
|
|
|
||
|
|
class Retire(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.tmp = Path(tempfile.mkdtemp())
|
||
|
|
(self.tmp / 'bin').mkdir()
|
||
|
|
(self.tmp / 'bin/bao').write_text(FAKE_BAO)
|
||
|
|
(self.tmp / 'bin/bao').chmod(0o755)
|
||
|
|
self.home = self.tmp / '.warden-attended-login' / 'session'
|
||
|
|
self.home.mkdir(parents=True)
|
||
|
|
self.state = {'calls': [], 'caller_policies': ['default', 'platform-admin'],
|
||
|
|
'kv': {'nextcloud': 2, 'nextcloud/backup': 3}}
|
||
|
|
|
||
|
|
def run_helper(self, *extra, confirm=CONFIRM):
|
||
|
|
(self.tmp / 'state.json').write_text(json.dumps(self.state))
|
||
|
|
receipt = self.tmp / f'r{len(list(self.tmp.glob("r*.json")))}.json'
|
||
|
|
env = {k: v for k, v in os.environ.items() if k not in ('BAO_TOKEN', 'VAULT_TOKEN')}
|
||
|
|
env.update(PATH=f'{self.tmp / "bin"}:{env["PATH"]}', HOME=str(self.home),
|
||
|
|
FAKE_BAO_STATE=str(self.tmp / 'state.json'))
|
||
|
|
proc = subprocess.run([sys.executable, str(SCRIPT), '--confirm', confirm, '--receipt', str(receipt), *extra],
|
||
|
|
env=env, capture_output=True, text=True)
|
||
|
|
self.assertEqual((proc.stdout, proc.stderr), ('', ''))
|
||
|
|
self.state = json.loads((self.tmp / 'state.json').read_text())
|
||
|
|
text = receipt.read_text()
|
||
|
|
self.assertNotIn(ONE_SHOT, text)
|
||
|
|
return proc.returncode, json.loads(text)
|
||
|
|
|
||
|
|
def assert_cleaned_up(self, receipt):
|
||
|
|
self.assertNotIn('policy', self.state)
|
||
|
|
self.assertTrue(receipt['policy_deleted'])
|
||
|
|
self.assertEqual(self.state.get('revoked'), ONE_SHOT)
|
||
|
|
|
||
|
|
def test_retires_entry_and_leaves_backup_untouched(self):
|
||
|
|
code, receipt = self.run_helper()
|
||
|
|
self.assertEqual((code, receipt['outcome']), (0, 'retired'))
|
||
|
|
self.assertNotIn('nextcloud', self.state['kv'])
|
||
|
|
self.assertEqual(self.state['kv']['nextcloud/backup'], 3)
|
||
|
|
self.assertEqual(receipt['entry_before'], {'current_version': 2, 'versions': 2})
|
||
|
|
self.assert_cleaned_up(receipt)
|
||
|
|
self.assertFalse(any(c['args'][0] == 'read' and '/data/' in c['args'][-1] for c in self.state['calls']))
|
||
|
|
|
||
|
|
def test_one_shot_policy_is_exact(self):
|
||
|
|
self.run_helper('--dry-run')
|
||
|
|
calls = [c for c in self.state['calls'] if c['args'][:2] == ['token', 'create']]
|
||
|
|
self.assertEqual(len(calls), 1)
|
||
|
|
args = calls[0]['args']
|
||
|
|
for flag in ('-orphan', '-policy=operators-retire-nextcloud', '-no-default-policy', '-ttl=60s'):
|
||
|
|
self.assertIn(flag, args)
|
||
|
|
|
||
|
|
def test_dry_run_deletes_nothing_and_cleans_up(self):
|
||
|
|
code, receipt = self.run_helper('--dry-run')
|
||
|
|
self.assertEqual((code, receipt['outcome']), (0, 'dry_run_would_retire'))
|
||
|
|
self.assertIn('nextcloud', self.state['kv'])
|
||
|
|
self.assert_cleaned_up(receipt)
|
||
|
|
|
||
|
|
def test_rerun_reports_already_absent(self):
|
||
|
|
self.state['kv'].pop('nextcloud')
|
||
|
|
code, receipt = self.run_helper()
|
||
|
|
self.assertEqual((code, receipt['outcome']), (0, 'already_absent'))
|
||
|
|
self.assert_cleaned_up(receipt)
|
||
|
|
|
||
|
|
def test_missing_sibling_refuses(self):
|
||
|
|
self.state['kv'].pop('nextcloud/backup')
|
||
|
|
code, receipt = self.run_helper()
|
||
|
|
self.assertEqual((code, receipt['outcome']), (2, 'sibling_absent_refusing_to_guess_layout'))
|
||
|
|
self.assertIn('nextcloud', self.state['kv'])
|
||
|
|
self.assert_cleaned_up(receipt)
|
||
|
|
|
||
|
|
def test_delete_failure_still_cleans_up(self):
|
||
|
|
self.state['fail_delete'] = True
|
||
|
|
code, receipt = self.run_helper()
|
||
|
|
self.assertEqual((code, receipt['outcome']), (4, 'delete_failed'))
|
||
|
|
self.assert_cleaned_up(receipt)
|
||
|
|
|
||
|
|
def test_refuses_without_confirmation_or_platform_admin(self):
|
||
|
|
code, receipt = self.run_helper(confirm='RETIRE')
|
||
|
|
self.assertEqual((code, receipt['outcome']), (2, 'exact_confirmation_required'))
|
||
|
|
self.state['caller_policies'] = ['operator-custody']
|
||
|
|
code, receipt = self.run_helper()
|
||
|
|
self.assertEqual((code, receipt['outcome']), (2, 'attended_platform_admin_required'))
|
||
|
|
self.assertFalse(any(c['args'][:2] == ['policy', 'write'] for c in self.state['calls']))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
unittest.main()
|