from __future__ import annotations import importlib.util import json import os import subprocess import tempfile from pathlib import Path import pytest pytestmark = pytest.mark.skipif(os.name != 'posix', reason='Linux controller ownership/flock contract') @pytest.fixture def binding(tmp_path, monkeypatch): path = Path(__file__).resolve().parents[2] / 'ops/gitea-deploy-broker/runner_statistics_binding.py' spec = importlib.util.spec_from_file_location('binding_test', path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) anchor = tmp_path / 'controller-anchor' state = anchor / 'state' (state / 'locks').mkdir(parents=True) source = tmp_path / 'brain' source.mkdir() data = tmp_path / 'appops' data.mkdir() monkeypatch.setattr(module, 'ROOT_UID', os.getuid()) if os.geteuid() != 0: # The production controller is root. Keep portable unit coverage for # non-root Linux runs while root-only tests assert 100:101. monkeypatch.setattr(module, 'APPOPS_UID', os.geteuid()) monkeypatch.setattr(module, 'APPOPS_GID', os.getegid()) for key, value in {'STATE_ROOT': state, 'BINDING_ROOT': state / 'runner-statistics-binding', 'PB_SOURCE': source, 'PB_ENV': source / '.env', 'APPOPS_DATA': data, 'TOKEN_FILE': data / 'runner-statistics-token', 'ADMIN_TOKEN_FILE': data / 'mcp-token', 'GLOBAL_LOCK': anchor / 'deploy.lock'}.items(): monkeypatch.setattr(module, key, value) module.PB_ENV.write_bytes(b'EXISTING_PRIVATE_VALUE=keep-me\nPROJECTBRAIN_APPOPS_MCP_URL=\nPROJECTBRAIN_APPOPS_MCP_TOKEN=\n') module.PB_ENV.chmod(0o600) module.ADMIN_TOKEN_FILE.write_text('admin-capability-separate-secret\n') module.ADMIN_TOKEN_FILE.chmod(0o600) module.project = {'jens/projectbrain': {'branch': 'main', 'slug': 'projectbrain', 'source_dir': str(source), 'containers': ['projectbrain'], 'deploy_script': 'scripts/deploy-unraid-single.sh', 'validation': {'required': True, 'contexts': ['Managed validation / full (push)']}}} return module def invoke(binding): return binding.configure(revision='a' * 40, projects=binding.project) def test_configuration_is_separate_private_and_idempotent(binding): before = binding.PB_ENV.read_bytes() result = invoke(binding) assert result['status'] == 'configured' assert result['projectbrainDeliveryRequired'] is True token = binding.TOKEN_FILE.read_text().strip() assert len(token) == 64 assert token != binding.ADMIN_TOKEN_FILE.read_text().strip() assert token not in json.dumps(result) assert binding._configured_values(binding.PB_ENV.read_bytes()) == { binding.URL_KEY: binding.ENDPOINT, binding.TOKEN_KEY: token} assert b'EXISTING_PRIVATE_VALUE=keep-me\n' in binding.PB_ENV.read_bytes() assert (binding.BINDING_ROOT / 'environment-before').read_bytes() == before for path in (binding.PB_ENV, binding.TOKEN_FILE, binding.BINDING_ROOT / 'receipt.json'): assert path.stat().st_mode & 0o777 == 0o600 token_info = binding.TOKEN_FILE.stat() assert (token_info.st_uid, token_info.st_gid) == (binding.APPOPS_UID, binding.APPOPS_GID) assert token_info.st_nlink == 1 assert invoke(binding)['status'] == 'already_configured' @pytest.mark.skipif(os.name != 'posix' or os.geteuid() != 0, reason='fixed AppOps ownership requires a privileged Linux controller') def test_statistics_token_is_appops_readable_while_binding_state_stays_root_private(binding): assert os.geteuid() != 100 result = invoke(binding) token_info = binding.TOKEN_FILE.stat() assert (token_info.st_uid, token_info.st_gid) == (100, 101) assert token_info.st_mode & 0o777 == 0o600 assert token_info.st_nlink == 1 assert len(binding.TOKEN_FILE.read_text().strip()) == 64 assert result['status'] == 'configured' for path in (binding.PB_ENV, binding.BINDING_ROOT / 'receipt.json', binding.BINDING_ROOT / 'environment-before'): info = path.stat() assert info.st_uid == binding.ROOT_UID assert info.st_mode & 0o777 == 0o600 assert info.st_nlink == 1 assert invoke(binding)['status'] == 'already_configured' token_info = binding.TOKEN_FILE.stat() assert (token_info.st_uid, token_info.st_gid) == (100, 101) assert token_info.st_mode & 0o777 == 0o600 assert token_info.st_nlink == 1 @pytest.mark.skipif(os.name != 'posix' or os.geteuid() != 0, reason='dropping to the fixed AppOps identity requires a privileged Linux controller') def test_private_secret_reader_accepts_statistics_token_as_dropped_appops_identity(binding): invoke(binding) repository = Path(__file__).resolve().parents[2] code = ( 'import sys; from pathlib import Path; ' 'sys.path.insert(0, sys.argv[2]); ' 'from backend.app.core.config import _private_secret_file; ' 'value = _private_secret_file(sys.argv[1]); ' 'raise SystemExit(0 if value is not None and len(value) == 64 else 1)' ) with tempfile.TemporaryDirectory(prefix='appops-statistics-reader-', dir='/tmp') as directory: os.chmod(directory, 0o711) token_copy = Path(directory) / 'runner-statistics-token' token_copy.write_bytes(binding.TOKEN_FILE.read_bytes()) source_info = binding.TOKEN_FILE.stat() os.chown(token_copy, source_info.st_uid, source_info.st_gid) token_copy.chmod(source_info.st_mode & 0o777) def drop_to_appops(): os.setgroups([]) os.setgid(101) os.setuid(100) result = subprocess.run( ['/usr/bin/python3', '-c', code, str(token_copy), str(repository)], cwd=directory, preexec_fn=drop_to_appops, capture_output=True, text=True, check=False, ) token = binding.TOKEN_FILE.read_text().strip() assert token not in result.stdout and token not in result.stderr assert result.stdout == '' assert result.returncode == 0, ( 'AppOps config private-secret reader rejected the fixed token: ' + result.stderr[-500:] ) @pytest.mark.skipif(os.name != 'posix' or os.geteuid() != 0, reason='wrong-owner validation requires a privileged Linux controller') def test_configured_root_owned_statistics_token_fails_closed(binding): assert invoke(binding)['status'] == 'configured' token = binding.TOKEN_FILE.read_bytes() os.chown(binding.TOKEN_FILE, binding.ROOT_UID, os.getgid()) with pytest.raises(binding.StatisticsBindingError, match='file_untrusted'): invoke(binding) assert binding.TOKEN_FILE.read_bytes() == token info = binding.TOKEN_FILE.stat() assert (info.st_uid, info.st_gid) == (binding.ROOT_UID, os.getgid()) @pytest.mark.parametrize('key', ['source_dir', 'deploy_script', 'containers', 'validation']) def test_unknown_project_configuration_is_rejected_before_write(binding, key): binding.project['jens/projectbrain'][key] = 'untrusted' with pytest.raises(binding.StatisticsBindingError): invoke(binding) assert not binding.TOKEN_FILE.exists() assert not binding.BINDING_ROOT.exists() def test_existing_operator_configuration_is_preserved(binding): before = b'PROJECTBRAIN_APPOPS_MCP_URL=https://operator.example/mcp\nOTHER=preserve\n' binding.PB_ENV.write_bytes(before) with pytest.raises(binding.StatisticsBindingError, match='existing_operator_configuration'): invoke(binding) assert binding.PB_ENV.read_bytes() == before assert not binding.TOKEN_FILE.exists() def test_existing_operator_token_is_never_adopted(binding): binding.TOKEN_FILE.write_text('existing-operator-secret') binding.TOKEN_FILE.chmod(0o600) os.chown(binding.TOKEN_FILE, binding.APPOPS_UID, binding.APPOPS_GID) with pytest.raises(binding.StatisticsBindingError, match='existing_operator_credential'): invoke(binding) assert binding.TOKEN_FILE.read_text() == 'existing-operator-secret' @pytest.mark.parametrize('fault_path', ['token', 'environment', 'receipt']) def test_crash_after_each_durable_boundary_recovers_same_credential(binding, monkeypatch, fault_path): atomic = binding._atomic interrupted = False def crash(path, value, expected): nonlocal interrupted atomic(path, value, expected) selected = {'token': binding.TOKEN_FILE, 'environment': binding.PB_ENV, 'receipt': binding.BINDING_ROOT / 'receipt.json'}[fault_path] if path == selected and not interrupted: interrupted = True raise OSError('simulated interruption after fsync') monkeypatch.setattr(binding, '_atomic', crash) with pytest.raises(OSError): invoke(binding) journal = json.loads((binding.BINDING_ROOT / 'receipt.json').read_bytes()) expected = journal['token'] monkeypatch.setattr(binding, '_atomic', atomic) assert invoke(binding)['status'] == 'configured' assert binding.TOKEN_FILE.read_text().strip() == expected @pytest.mark.skipif(os.name != 'posix' or os.geteuid() != 0, reason='simulated ownership failure requires a privileged Linux controller') def test_statistics_token_owner_failure_closes_fd_and_never_publishes(binding, monkeypatch, capsys): opened_token_fds = [] def fail_chown(fd, uid, gid): opened_token_fds.append(fd) raise PermissionError('simulated AppOps ownership failure') monkeypatch.setattr(binding.os, 'fchown', fail_chown) with pytest.raises(PermissionError): invoke(binding) assert len(opened_token_fds) == 1 with pytest.raises(OSError): binding.os.fstat(opened_token_fds[0]) assert not binding.TOKEN_FILE.exists() assert [path.name for path in binding.APPOPS_DATA.iterdir()] == ['mcp-token'] assert capsys.readouterr().out == '' def test_crash_after_backup_before_receipt_has_safe_retry(binding, monkeypatch): atomic = binding._atomic def crash(path, value, expected): atomic(path, value, expected) if path.name == 'environment-before': raise OSError('interruption after backup') monkeypatch.setattr(binding, '_atomic', crash) with pytest.raises(OSError): invoke(binding) monkeypatch.setattr(binding, '_atomic', atomic) assert invoke(binding)['status'] == 'configured' def test_pending_environment_cas_conflict_preserves_operator_change(binding, monkeypatch): atomic = binding._atomic def crash(path, value, expected): atomic(path, value, expected) if path == binding.TOKEN_FILE: raise OSError('interruption') monkeypatch.setattr(binding, '_atomic', crash) with pytest.raises(OSError): invoke(binding) binding.PB_ENV.write_bytes(b'OPERATOR_CHANGED=value\n') monkeypatch.setattr(binding, '_atomic', atomic) with pytest.raises(binding.StatisticsBindingError, match='environment_cas_conflict'): invoke(binding) assert binding.PB_ENV.read_bytes() == b'OPERATOR_CHANGED=value\n' def test_completed_binding_keeps_unrelated_later_operator_edits(binding): invoke(binding) with binding.PB_ENV.open('ab') as stream: stream.write(b'NEW_OPERATOR_VALUE=preserved\n') before = binding.PB_ENV.read_bytes() assert invoke(binding)['status'] == 'already_configured' assert binding.PB_ENV.read_bytes() == before @pytest.mark.parametrize('unsafe', ['symlink', 'public_file', 'writable_anchor', 'duplicate_key']) def test_unsafe_configuration_blocks_before_credential_write(binding, unsafe): if unsafe == 'symlink': target = binding.PB_ENV.with_name('other.env') binding.PB_ENV.rename(target) binding.PB_ENV.symlink_to(target) elif unsafe == 'public_file': binding.PB_ENV.chmod(0o644) elif unsafe == 'writable_anchor': binding.PB_SOURCE.chmod(0o777) else: binding.PB_ENV.write_text(f'{binding.URL_KEY}=\n{binding.URL_KEY}=\n') with pytest.raises((binding.StatisticsBindingError, OSError)): invoke(binding) assert not binding.TOKEN_FILE.exists() def test_active_deployment_lock_blocks_without_configuration_changes(binding): import fcntl with binding.GLOBAL_LOCK.open('a') as lock: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) with pytest.raises(binding.StatisticsBindingError, match='deployment_busy'): invoke(binding) assert not binding.TOKEN_FILE.exists() @pytest.mark.parametrize('owner,accepted', [(100, True), (200, False)]) def test_existing_fixed_admin_file_owner_contract(binding, monkeypatch, owner, accepted): actual = binding.os.fstat inode = binding.ADMIN_TOKEN_FILE.stat().st_ino def metadata(fd): info = actual(fd) if info.st_ino == inode: fields = list(info) fields[4] = owner return os.stat_result(fields) return info monkeypatch.setattr(binding.os, 'fstat', metadata) if accepted: assert invoke(binding)['status'] == 'configured' else: with pytest.raises(binding.StatisticsBindingError, match='file_untrusted'): invoke(binding) assert not binding.TOKEN_FILE.exists() def test_parent_swap_cannot_redirect_a_root_read(binding, monkeypatch): actual_open = binding.os.open original = binding.PB_ENV.read_bytes() moved = binding.PB_SOURCE.with_name('moved-brain') attacker = binding.PB_SOURCE.with_name('attacker') attacker.mkdir() (attacker / '.env').write_bytes(b'ATTACKER=must-not-be-read\n') (attacker / '.env').chmod(0o600) swapped = False def swap(path, flags, *args, **kwargs): nonlocal swapped if path == '.env' and 'dir_fd' in kwargs and not swapped: swapped = True binding.PB_SOURCE.rename(moved) binding.PB_SOURCE.symlink_to(attacker, target_is_directory=True) return actual_open(path, flags, *args, **kwargs) monkeypatch.setattr(binding.os, 'open', swap) assert binding._read(binding.PB_ENV, binding.MAX_ENV_BYTES) == original assert (attacker / '.env').read_bytes() == b'ATTACKER=must-not-be-read\n' with pytest.raises(OSError): binding._read(binding.PB_ENV, binding.MAX_ENV_BYTES) def test_parent_swap_cannot_redirect_atomic_replace(binding, monkeypatch): actual_replace = binding.os.replace before = binding.PB_ENV.read_bytes() updated = before + b'APPENDED=private\n' moved = binding.PB_SOURCE.with_name('moved-brain') attacker = binding.PB_SOURCE.with_name('attacker') attacker.mkdir() (attacker / '.env').write_bytes(b'ATTACKER=must-not-be-overwritten\n') (attacker / '.env').chmod(0o600) def swap(source, destination, **kwargs): assert source.startswith('.statistics-') assert destination == '.env' assert kwargs['src_dir_fd'] == kwargs['dst_dir_fd'] binding.PB_SOURCE.rename(moved) binding.PB_SOURCE.symlink_to(attacker, target_is_directory=True) return actual_replace(source, destination, **kwargs) monkeypatch.setattr(binding.os, 'replace', swap) binding._atomic(binding.PB_ENV, updated, before) assert (moved / '.env').read_bytes() == updated assert (attacker / '.env').read_bytes() == b'ATTACKER=must-not-be-overwritten\n' with pytest.raises(OSError): binding._read(binding.PB_ENV, binding.MAX_ENV_BYTES) def test_parent_swap_cannot_replace_the_authentication_secret(binding, monkeypatch): secret = b'original-administration-capability-1234567890' binding.ADMIN_TOKEN_FILE.write_bytes(secret) actual_open = binding.os.open moved = binding.APPOPS_DATA.with_name('moved-appops') attacker = binding.APPOPS_DATA.with_name('attacker-appops') attacker.mkdir() (attacker / 'mcp-token').write_bytes(b'attacker-chosen-authentication-capability-12345') (attacker / 'mcp-token').chmod(0o600) swapped = False def swap(path, flags, *args, **kwargs): nonlocal swapped if path == 'mcp-token' and 'dir_fd' in kwargs and not swapped: swapped = True binding.APPOPS_DATA.rename(moved) binding.APPOPS_DATA.symlink_to(attacker, target_is_directory=True) return actual_open(path, flags, *args, **kwargs) monkeypatch.setattr(binding.os, 'open', swap) assert binding.read_administration_secret() == secret assert binding.read_administration_secret() is None