2026-10-14 01:17:35 +02:00
|
|
|
"""Credential boundaries, exact group effects and honest diagnostic outcomes."""
|
|
|
|
|
import contextlib
|
|
|
|
|
import copy
|
|
|
|
|
import importlib.util
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
SPEC = importlib.util.spec_from_file_location('lldap_admin', ROOT / 'sso-mfa/k8s/lldap/lldap_admin.py')
|
|
|
|
|
admin = importlib.util.module_from_spec(SPEC)
|
|
|
|
|
SPEC.loader.exec_module(admin)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('script', ['lldap/bootstrap-users.sh', 'lldap/manage-group-members.sh', 'verify-t07.sh'])
|
|
|
|
|
def test_legacy_arguments_never_source_credentials_or_invoke_external_tools(tmp_path, script):
|
|
|
|
|
marker = tmp_path / 'sourced'
|
|
|
|
|
calls = tmp_path / 'calls'
|
|
|
|
|
root = tmp_path / 'private'
|
|
|
|
|
(root / 'lldap').mkdir(parents=True, mode=0o700)
|
|
|
|
|
(root / 'lldap/secrets.env').write_text(f'LLDAP_LDAP_USER_PASS=synthetic-private-marker\ntouch "{marker}"\n')
|
|
|
|
|
commands = tmp_path / 'bin'
|
|
|
|
|
commands.mkdir()
|
|
|
|
|
for name in ('curl', 'kubectl'):
|
|
|
|
|
executable = commands / name
|
|
|
|
|
executable.write_text(f'#!/bin/sh\nprintf called >> "{calls}"\nexit 1\n')
|
|
|
|
|
executable.chmod(0o700)
|
|
|
|
|
args = ['http://127.0.0.1:1', str(root)]
|
|
|
|
|
if 'manage-group' in script:
|
|
|
|
|
args = ['add', 'test-user', 'activity-core-operators', *args]
|
|
|
|
|
env = dict(os.environ, PATH=str(commands) + ':' + os.environ['PATH'])
|
|
|
|
|
for key in ('LLDAP_ADMIN_PASS', 'BASH_ENV'):
|
|
|
|
|
env.pop(key, None)
|
|
|
|
|
result = subprocess.run(['bash', '-x', str(ROOT / 'sso-mfa/k8s' / script), *args],
|
|
|
|
|
env=env, capture_output=True, text=True, timeout=10)
|
|
|
|
|
assert result.returncode != 0
|
|
|
|
|
assert not marker.exists() and not calls.exists()
|
|
|
|
|
assert 'synthetic-private-marker' not in result.stdout + result.stderr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Directory:
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.state = {'users': ['agent-codex', 'break-glass', 'tegwick'], 'groups': {
|
|
|
|
|
'lldap_admin': {'id': 1, 'members': ['tegwick']},
|
|
|
|
|
'net-kingdom-admins': {'id': 2, 'members': ['break-glass']},
|
|
|
|
|
'activity-core-operators': {'id': 3, 'members': ['tegwick']},
|
|
|
|
|
'tenant:one:users': {'id': 4, 'members': ['agent-codex']},
|
|
|
|
|
}}
|
|
|
|
|
self.writes = []
|
|
|
|
|
self.login = 0
|
|
|
|
|
self.reads = 0
|
|
|
|
|
self.drop_write = False
|
|
|
|
|
self.extra_effect = False
|
|
|
|
|
self.drift_at = None
|
|
|
|
|
self.malformed = None
|
|
|
|
|
|
|
|
|
|
def _login(self):
|
|
|
|
|
self.login += 1
|
|
|
|
|
return 'fixture-token'
|
|
|
|
|
|
|
|
|
|
def _gql(self, token, query, variables):
|
|
|
|
|
assert query == 'query { users { id } groups { id displayName users { id } } }'
|
|
|
|
|
self.reads += 1
|
|
|
|
|
if self.reads == self.drift_at:
|
|
|
|
|
self.state['groups']['tenant:one:users']['members'] = []
|
|
|
|
|
if self.malformed is not None:
|
|
|
|
|
return self.malformed
|
|
|
|
|
return {'users': [{'id': u} for u in self.state['users']], 'groups': [
|
|
|
|
|
{'id': g['id'], 'displayName': name, 'users': [{'id': u} for u in g['members']]}
|
|
|
|
|
for name, g in self.state['groups'].items()]}
|
|
|
|
|
|
|
|
|
|
def _ensure_group(self, token, groups, name):
|
|
|
|
|
self.writes.append(('create', name))
|
|
|
|
|
if not self.drop_write:
|
|
|
|
|
self.state['groups'][name] = {'id': max(g['id'] for g in self.state['groups'].values()) + 1, 'members': []}
|
|
|
|
|
|
|
|
|
|
def _add_group(self, token, user, gid):
|
|
|
|
|
self._write('add', user, gid)
|
|
|
|
|
|
|
|
|
|
def _remove_group(self, token, user, gid):
|
|
|
|
|
self._write('remove', user, gid)
|
|
|
|
|
|
|
|
|
|
def _write(self, action, user, gid):
|
|
|
|
|
self.writes.append((action, user, gid))
|
|
|
|
|
if self.drop_write:
|
|
|
|
|
return
|
|
|
|
|
group = next(g for g in self.state['groups'].values() if g['id'] == gid)
|
|
|
|
|
members = set(group['members'])
|
|
|
|
|
(members.add if action == 'add' else members.discard)(user)
|
|
|
|
|
group['members'] = sorted(members)
|
|
|
|
|
if self.extra_effect:
|
|
|
|
|
self.state['groups']['tenant:one:users']['members'] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def change(provider, **kwargs):
|
|
|
|
|
return admin.maintain(provider, 'https://lldap.coulomb.social', 'membership', **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bootstrap_preview_apply_and_idempotence_preserve_existing_state():
|
|
|
|
|
provider = Directory()
|
|
|
|
|
before = copy.deepcopy(provider.state)
|
|
|
|
|
preview = admin.maintain(provider, 'https://lldap.coulomb.social', 'bootstrap')
|
|
|
|
|
assert preview['changes'] == [{'create_group': 'net-kingdom-users'}]
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
result = admin.maintain(provider, 'https://lldap.coulomb.social', 'bootstrap', apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert result['result'] == 'verified'
|
|
|
|
|
assert provider.state['users'] == before['users']
|
|
|
|
|
for name, group in before['groups'].items():
|
|
|
|
|
assert provider.state['groups'][name] == group
|
|
|
|
|
assert provider.state['groups']['net-kingdom-users']['members'] == []
|
|
|
|
|
provider.writes.clear()
|
|
|
|
|
preview = admin.maintain(provider, 'https://lldap.coulomb.social', 'bootstrap')
|
|
|
|
|
admin.maintain(provider, 'https://lldap.coulomb.social', 'bootstrap', apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('action,user', [('add', 'agent-codex'), ('remove', 'tegwick')])
|
|
|
|
|
def test_membership_changes_only_exact_member(action, user):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
before = copy.deepcopy(provider.state)
|
|
|
|
|
kwargs = {'action': action, 'group': 'activity-core-operators', 'user': user}
|
|
|
|
|
preview = change(provider, **kwargs)
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
result = change(provider, **kwargs, apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert result['result'] == 'verified'
|
|
|
|
|
assert (user in provider.state['groups']['activity-core-operators']['members']) == (action == 'add')
|
|
|
|
|
for name in set(before['groups']) - {'activity-core-operators'}:
|
|
|
|
|
assert provider.state['groups'][name] == before['groups'][name]
|
|
|
|
|
provider.writes.clear()
|
|
|
|
|
preview = change(provider, **kwargs)
|
|
|
|
|
change(provider, **kwargs, apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('group', ['net-kingdom-admins', 'net-kingdom-users', 'netkingdom-suspended', 'tenant-one-users', 'lldap_admin'])
|
|
|
|
|
@pytest.mark.parametrize('action', ['add', 'remove', 'list'])
|
|
|
|
|
def test_reserved_groups_refuse_before_login(group, action):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
with pytest.raises(Exception):
|
|
|
|
|
change(provider, action=action, group=group, user=None if action == 'list' else 'agent-codex')
|
|
|
|
|
assert provider.login == 0 and provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['changed_state', 'changed_action', 'changed_origin', 'second_read_drift', 'missing_group', 'missing_user'])
|
|
|
|
|
def test_stale_or_ambiguous_requests_never_mutate(kind):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
kwargs = {'action': 'add', 'group': 'activity-core-operators', 'user': 'agent-codex'}
|
|
|
|
|
preview = change(provider, **kwargs)
|
|
|
|
|
base = 'https://lldap.coulomb.social'
|
|
|
|
|
if kind == 'changed_state': provider.state['groups']['tenant:one:users']['members'] = []
|
|
|
|
|
if kind == 'changed_action': kwargs['action'] = 'remove'
|
|
|
|
|
if kind == 'changed_origin': base = 'http://127.0.0.1:10000'
|
|
|
|
|
if kind == 'second_read_drift': provider.drift_at = 3
|
|
|
|
|
if kind == 'missing_group': kwargs['group'] = 'missing-group'
|
|
|
|
|
if kind == 'missing_user': kwargs['user'] = 'missing-user'
|
|
|
|
|
with pytest.raises(Exception):
|
|
|
|
|
admin.maintain(provider, base, 'membership', **kwargs, apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('failure', ['drop_write', 'extra_effect'])
|
|
|
|
|
def test_failed_or_broader_write_cannot_report_success(failure):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
kwargs = {'action': 'add', 'group': 'activity-core-operators', 'user': 'agent-codex'}
|
|
|
|
|
preview = change(provider, **kwargs)
|
|
|
|
|
setattr(provider, failure, True)
|
|
|
|
|
with pytest.raises(admin.Refused):
|
|
|
|
|
change(provider, **kwargs, apply=True, expected_digest=preview['snapshot_sha256'])
|
|
|
|
|
assert len(provider.writes) == 1 # No blind rollback/retry.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_list_is_nonmutating_and_verification_does_not_claim_acceptance():
|
|
|
|
|
provider = Directory()
|
|
|
|
|
listing = change(provider, action='list', group='activity-core-operators')
|
|
|
|
|
assert listing['members'] == ['tegwick']
|
|
|
|
|
result = admin.maintain(provider, 'https://lldap.coulomb.social', 'verify')
|
|
|
|
|
assert result['native_acceptance'] == 'unverified'
|
|
|
|
|
assert result['baseline_groups_present']['net-kingdom-users'] is False
|
|
|
|
|
assert 'independent_recovery' in result['remaining']
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
2026-10-14 23:41:32 +02:00
|
|
|
def test_groups_preview_lists_display_names_for_one_uid_without_writes():
|
|
|
|
|
provider = Directory()
|
|
|
|
|
result = admin.maintain(provider, 'https://lldap.coulomb.social', 'groups', user='tegwick')
|
|
|
|
|
assert result == {'result': 'groups_observed', 'uid': 'tegwick',
|
|
|
|
|
'groups': ['activity-core-operators', 'lldap_admin'], 'native_acceptance': 'unverified'}
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('kwargs', [{'user': 'nobody'}, {'user': 'Bad uid'}, {'user': None},
|
|
|
|
|
{'user': 'tegwick', 'group': 'lldap_admin'},
|
|
|
|
|
{'user': 'tegwick', 'apply': True, 'expected_digest': '0' * 64}])
|
|
|
|
|
def test_groups_preview_refuses_unknown_or_malformed_requests(kwargs):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
with pytest.raises(admin.Refused):
|
|
|
|
|
admin.maintain(provider, 'https://lldap.coulomb.social', 'groups', **kwargs)
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_groups_cli_has_no_apply_and_prints_only_diagnosis_fields():
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
with patch.object(admin, 'private_password', return_value='fixture'), patch.object(admin, 'LLDAPProvisioner', return_value=Directory()), contextlib.redirect_stdout(out):
|
|
|
|
|
assert admin.main(['groups', '--root', '/fixture', '--user', 'agent-codex']) == 0
|
|
|
|
|
assert json.loads(out.getvalue()) == {'groups': ['tenant:one:users'], 'native_acceptance': 'unverified',
|
|
|
|
|
'result': 'groups_observed', 'uid': 'agent-codex'}
|
|
|
|
|
with contextlib.redirect_stderr(io.StringIO()):
|
|
|
|
|
assert admin.main(['groups', '--root', '/fixture', '--user', 'agent-codex', '--apply']) == 1
|
|
|
|
|
|
|
|
|
|
|
2026-10-14 01:17:35 +02:00
|
|
|
@pytest.mark.parametrize('payload', [{}, {'users': [], 'groups': None}, {'users': [{'id': 'same'}, {'id': 'same'}], 'groups': []},
|
|
|
|
|
{'users': [], 'groups': [{'id': True, 'displayName': 'a', 'users': []}]},
|
|
|
|
|
{'users': [], 'groups': [{'id': 1, 'displayName': 'a', 'users': [{'id': 'unknown'}]}]},
|
|
|
|
|
{'users': [], 'groups': [{'id': 1, 'displayName': 'a', 'users': []}, {'id': 1, 'displayName': 'b', 'users': []}]}])
|
|
|
|
|
def test_malformed_directory_snapshot_refused(payload):
|
|
|
|
|
provider = Directory()
|
|
|
|
|
provider.malformed = payload
|
|
|
|
|
with pytest.raises(admin.Refused):
|
|
|
|
|
admin.maintain(provider, 'https://lldap.coulomb.social', 'bootstrap')
|
|
|
|
|
assert provider.writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('url', ['http://lldap.coulomb.social', 'https://evil.test', 'http://localhost:17170',
|
|
|
|
|
'https://lldap.coulomb.social@evil.test', 'http://127.0.0.1:80/path',
|
|
|
|
|
'http://127.0.0.1:80?secret=marker', 'https://lldap.coulomb.social/#marker'])
|
|
|
|
|
def test_unapproved_origin_refused_before_private_input(url):
|
|
|
|
|
with patch.object(admin, 'private_password') as private, contextlib.redirect_stderr(io.StringIO()):
|
|
|
|
|
assert admin.main(['bootstrap', '--root', '/not-read', '--lldap-url', url]) == 1
|
|
|
|
|
private.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_verification_cli_always_returns_incomplete_for_structure_only():
|
|
|
|
|
with patch.object(admin, 'private_password', return_value='fixture'), patch.object(admin, 'LLDAPProvisioner', return_value=Directory()), contextlib.redirect_stdout(io.StringIO()):
|
|
|
|
|
assert admin.main(['verify', '--root', '/fixture']) == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_private_env_is_literal_not_executable(tmp_path):
|
|
|
|
|
root = tmp_path / 'private'
|
|
|
|
|
root.mkdir(mode=0o700)
|
|
|
|
|
(root / 'lldap').mkdir(mode=0o700)
|
|
|
|
|
marker = tmp_path / 'executed'
|
|
|
|
|
value = f'$(touch {marker})'
|
|
|
|
|
source = root / 'lldap/secrets.env'
|
|
|
|
|
source.write_text('LLDAP_LDAP_USER_PASS=' + value + '\n')
|
|
|
|
|
source.chmod(0o600)
|
|
|
|
|
assert admin.private_password(root) == value
|
|
|
|
|
assert not marker.exists()
|
|
|
|
|
source.chmod(0o644)
|
|
|
|
|
with pytest.raises(Exception): admin.private_password(root)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_provider_errors_are_not_printed():
|
|
|
|
|
output = io.StringIO()
|
|
|
|
|
with patch.object(admin, 'private_password', side_effect=RuntimeError('synthetic-secret-marker')), contextlib.redirect_stderr(output):
|
|
|
|
|
assert admin.main(['bootstrap', '--root', '/fixture']) == 1
|
|
|
|
|
assert 'synthetic-secret-marker' not in output.getvalue()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def http_directory():
|
|
|
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
|
import threading
|
|
|
|
|
|
|
|
|
|
state = {'mode': 'ok', 'calls': [], 'redirect_hits': 0, 'directory': Directory()}
|
|
|
|
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
|
|
|
def log_message(self, *args):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def do_POST(self):
|
|
|
|
|
payload = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
|
|
|
|
|
state['calls'].append((self.path, payload))
|
|
|
|
|
if self.path == '/redirected':
|
|
|
|
|
state['redirect_hits'] += 1
|
|
|
|
|
if state['mode'] == 'redirect':
|
|
|
|
|
self.send_response(307)
|
|
|
|
|
self.send_header('Location', '/redirected')
|
|
|
|
|
self.end_headers()
|
|
|
|
|
return
|
|
|
|
|
if state['mode'] == 'denied':
|
|
|
|
|
self.send_response(403)
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(b'upstream-secret-marker')
|
|
|
|
|
return
|
|
|
|
|
if state['mode'] == 'oversize': raw = b' ' * (admin.MAX_BODY + 1)
|
|
|
|
|
elif state['mode'] == 'duplicate': raw = b'{"token":"first", "token":"second"}'
|
|
|
|
|
elif state['mode'] == 'invalid': raw = b'upstream-secret-marker'
|
|
|
|
|
elif self.path == '/auth/simple/login': raw = b'{"token":"synthetic-token"}'
|
|
|
|
|
else:
|
|
|
|
|
assert self.headers['Authorization'] == 'Bearer synthetic-token'
|
|
|
|
|
if state['mode'] == 'graphql_error': result = {'errors': [{'message': 'upstream-secret-marker'}]}
|
|
|
|
|
else: result = {'data': state['directory']._gql('', payload['query'], payload.get('variables'))}
|
|
|
|
|
raw = json.dumps(result).encode()
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
self.send_header('Content-Type', 'application/json')
|
|
|
|
|
self.send_header('Content-Length', str(len(raw)))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
try: self.wfile.write(raw)
|
|
|
|
|
except (BrokenPipeError, ConnectionResetError): pass
|
|
|
|
|
|
|
|
|
|
server = ThreadingHTTPServer(('127.0.0.1', 0), Handler)
|
|
|
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
|
|
|
thread.start()
|
|
|
|
|
try:
|
|
|
|
|
yield f'http://127.0.0.1:{server.server_port}', state
|
|
|
|
|
finally:
|
|
|
|
|
server.shutdown()
|
|
|
|
|
server.server_close()
|
|
|
|
|
thread.join()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def private_root(tmp_path):
|
|
|
|
|
root = tmp_path / 'private'
|
|
|
|
|
root.mkdir(mode=0o700)
|
|
|
|
|
(root / 'lldap').mkdir(mode=0o700)
|
|
|
|
|
source = root / 'lldap/secrets.env'
|
|
|
|
|
source.write_text('LLDAP_LDAP_USER_PASS=synthetic-private-marker\n')
|
|
|
|
|
source.chmod(0o600)
|
|
|
|
|
return root
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('mode', ['redirect', 'denied', 'oversize', 'duplicate', 'invalid', 'graphql_error'])
|
|
|
|
|
def test_http_failure_does_not_forward_credentials_or_render_provider_body(tmp_path, http_directory, mode):
|
|
|
|
|
base, state = http_directory
|
|
|
|
|
state['mode'] = mode
|
|
|
|
|
output, error = io.StringIO(), io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(error):
|
|
|
|
|
code = admin.main(['bootstrap', '--root', str(private_root(tmp_path)), '--lldap-url', base])
|
|
|
|
|
assert code == 1
|
|
|
|
|
assert state['redirect_hits'] == 0
|
|
|
|
|
assert 'synthetic-private-marker' not in output.getvalue() + error.getvalue()
|
|
|
|
|
assert 'upstream-secret-marker' not in output.getvalue() + error.getvalue()
|
|
|
|
|
assert json.loads(error.getvalue())['result'] == 'unverified'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_loopback_http_uses_explicit_origin_and_ignores_ambient_proxy(tmp_path, http_directory):
|
|
|
|
|
base, state = http_directory
|
|
|
|
|
output = io.StringIO()
|
|
|
|
|
with patch.dict(os.environ, {'http_proxy': 'http://127.0.0.1:1', 'HTTP_PROXY': 'http://127.0.0.1:1', 'no_proxy': '', 'NO_PROXY': ''}), contextlib.redirect_stdout(output):
|
|
|
|
|
code = admin.main(['bootstrap', '--root', str(private_root(tmp_path)), '--lldap-url', base])
|
|
|
|
|
assert code == 0
|
|
|
|
|
assert json.loads(output.getvalue())['result'] == 'preview'
|
|
|
|
|
assert len(state['calls']) == 2
|
|
|
|
|
assert state['directory'].writes == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('defect', ['symlink_file', 'hardlink_file', 'public_parent', 'duplicate_field', 'git_root'])
|
|
|
|
|
def test_private_input_boundary_refuses_unsafe_custody(tmp_path, defect):
|
|
|
|
|
root = private_root(tmp_path)
|
|
|
|
|
source = root / 'lldap/secrets.env'
|
|
|
|
|
if defect == 'symlink_file':
|
|
|
|
|
original = root / 'lldap/original'
|
|
|
|
|
source.rename(original)
|
|
|
|
|
source.symlink_to(original)
|
|
|
|
|
elif defect == 'hardlink_file':
|
|
|
|
|
os.link(source, root / 'lldap/alias')
|
|
|
|
|
elif defect == 'public_parent':
|
|
|
|
|
root.chmod(0o755)
|
|
|
|
|
elif defect == 'duplicate_field':
|
|
|
|
|
source.write_text(source.read_text() + 'LLDAP_LDAP_USER_PASS=second\n')
|
|
|
|
|
elif defect == 'git_root':
|
|
|
|
|
(root / '.git').mkdir()
|
|
|
|
|
(root / '.git/HEAD').write_text('ref: refs/heads/main\n')
|
|
|
|
|
with patch.object(admin, 'LLDAPProvisioner') as provider, contextlib.redirect_stderr(io.StringIO()):
|
|
|
|
|
assert admin.main(['bootstrap', '--root', str(root)]) == 1
|
|
|
|
|
provider.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('extra', [['--apply'], ['--expected-digest', '0' * 64], ['--apply', '--expected-digest', 'not-a-digest']])
|
|
|
|
|
def test_apply_binding_errors_refuse_before_credential_read(extra):
|
|
|
|
|
with patch.object(admin, 'private_password') as private, contextlib.redirect_stderr(io.StringIO()):
|
|
|
|
|
assert admin.main(['bootstrap', '--root', '/not-read', *extra]) == 1
|
|
|
|
|
private.assert_not_called()
|