106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
|
|
import importlib.util
|
||
|
|
import json
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
spec = importlib.util.spec_from_file_location(
|
||
|
|
'check', Path(__file__).resolve().parents[1] / 'scripts/openbao_platform_admin_check.py')
|
||
|
|
m = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(m)
|
||
|
|
|
||
|
|
DECLARED = m.POLICY_FILE.read_text(encoding='utf-8')
|
||
|
|
FULL = {p: ['create', 'delete', 'list', 'read', 'sudo', 'update'] for p in m.SELF_PATHS}
|
||
|
|
|
||
|
|
|
||
|
|
def token(**extra):
|
||
|
|
return dict({'policies': ['default', 'platform-admin'], 'identity_policies': None,
|
||
|
|
'ttl': 3500, 'creation_ttl': 3600, 'type': 'service',
|
||
|
|
'path': 'auth/netkingdom/oidc/callback'}, **extra)
|
||
|
|
|
||
|
|
|
||
|
|
def test_matching_policy_and_capabilities_pass():
|
||
|
|
result = m.assess({'role_type': 'oidc'}, DECLARED + '\n', token(), FULL)
|
||
|
|
assert result['policy']['matches_declared']
|
||
|
|
assert result['self_revocation_permitted']
|
||
|
|
assert result['token']['has_default_policy']
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_self_capabilities_are_reported():
|
||
|
|
caps = {'auth/token/revoke-self': ['deny'], 'auth/token/lookup-self': ['read']}
|
||
|
|
result = m.assess({}, 'path "x" {}', token(policies=['platform-admin']), caps)
|
||
|
|
assert not result['policy']['matches_declared']
|
||
|
|
assert result['self_capabilities_missing'] == ['auth/token/revoke-self']
|
||
|
|
assert not result['token']['has_default_policy']
|
||
|
|
|
||
|
|
|
||
|
|
def test_bao_json_places_format_flag_before_positionals(monkeypatch):
|
||
|
|
seen = []
|
||
|
|
|
||
|
|
class Done:
|
||
|
|
stdout = b'{"data": {}}'
|
||
|
|
|
||
|
|
monkeypatch.setattr(m.subprocess, 'run', lambda cmd, **kw: seen.append(cmd) or Done())
|
||
|
|
m.bao_json('read', 'auth/token/lookup-self')
|
||
|
|
m.bao_json('write', 'sys/capabilities-self', 'paths=a,b')
|
||
|
|
assert seen == [['bao', 'read', '-format=json', 'auth/token/lookup-self'],
|
||
|
|
['bao', 'write', '-format=json', 'sys/capabilities-self', 'paths=a,b']]
|
||
|
|
|
||
|
|
|
||
|
|
def test_token_id_is_never_kept(monkeypatch):
|
||
|
|
monkeypatch.setattr(m, 'bao_json', lambda *a: {'data': {'id': 'SECRET', 'accessor': 'ACC', 'policies': ['default']}})
|
||
|
|
kept = m.read_token()
|
||
|
|
assert 'SECRET' not in json.dumps(kept) and 'ACC' not in json.dumps(kept)
|
||
|
|
|
||
|
|
|
||
|
|
def test_apply_only_when_drifted_and_verified(monkeypatch):
|
||
|
|
live = {'rules': 'path "old" {}'}
|
||
|
|
writes = []
|
||
|
|
monkeypatch.setattr(m, 'read_role', lambda: {})
|
||
|
|
monkeypatch.setattr(m, 'read_policy', lambda: live['rules'])
|
||
|
|
monkeypatch.setattr(m, 'read_token', token)
|
||
|
|
monkeypatch.setattr(m, 'read_capabilities', lambda: FULL)
|
||
|
|
monkeypatch.setattr(m, 'apply_policy', lambda: writes.append(1) or live.update(rules=DECLARED))
|
||
|
|
result, changed = m.run(apply=True)
|
||
|
|
assert changed and writes == [1] and result['policy']['matches_declared']
|
||
|
|
_, changed = m.run(apply=True)
|
||
|
|
assert not changed and writes == [1]
|
||
|
|
|
||
|
|
|
||
|
|
def test_refuses_outside_attended_envelope(tmp_path, monkeypatch):
|
||
|
|
monkeypatch.setenv('HOME', str(tmp_path))
|
||
|
|
receipt = tmp_path / 'r.json'
|
||
|
|
assert m.main(['--receipt', str(receipt)]) == 1
|
||
|
|
assert json.loads(receipt.read_text())['status'] == 'attended_envelope_required'
|
||
|
|
|
||
|
|
|
||
|
|
def test_failed_step_is_recorded_and_others_continue(monkeypatch):
|
||
|
|
def denied():
|
||
|
|
raise m.subprocess.CalledProcessError(
|
||
|
|
2, ['bao'], stderr=b'Error\nURL: GET http://127.0.0.1:18200/v1/auth/token/lookup-self\nCode: 403. Errors:\n\n* permission denied\n')
|
||
|
|
monkeypatch.setattr(m, 'read_role', lambda: {'role_type': 'oidc'})
|
||
|
|
monkeypatch.setattr(m, 'read_policy', lambda: DECLARED)
|
||
|
|
monkeypatch.setattr(m, 'read_token', denied)
|
||
|
|
monkeypatch.setattr(m, 'read_capabilities', lambda: FULL)
|
||
|
|
result, changed = m.run()
|
||
|
|
assert not changed
|
||
|
|
assert result['steps']['token_lookup_self'] == {
|
||
|
|
'ok': False, 'error': 'exit_2',
|
||
|
|
'detail': ['URL: GET http://127.0.0.1:18200/v1/auth/token/lookup-self',
|
||
|
|
'Code: 403. Errors:', '* permission denied']}
|
||
|
|
assert result['steps']['read_role']['ok'] and result['policy']['matches_declared']
|
||
|
|
|
||
|
|
|
||
|
|
def test_local_error_line_is_kept_with_token_shapes_redacted():
|
||
|
|
error = m.subprocess.CalledProcessError(
|
||
|
|
1, ['bao'], stderr=b'error looking up token hvs.CAESIabcdefghijklmnopqrstu: bad\n')
|
||
|
|
detail = m.error_summary(error)['detail']
|
||
|
|
assert detail == ['error looking up token [redacted]: bad']
|
||
|
|
|
||
|
|
|
||
|
|
def test_attached_rules_include_default(monkeypatch):
|
||
|
|
monkeypatch.setattr(m, 'read_rules', lambda name: 'rules-' + name)
|
||
|
|
rules = m.read_attached_rules({'token_policies': ['platform-admin', 'operator-custody']})
|
||
|
|
assert rules == {'default': 'rules-default', 'operator-custody': 'rules-operator-custody',
|
||
|
|
'platform-admin': 'rules-platform-admin'}
|