Files
truf-server/tests/test_runtime_security.py
T
2026-09-30 20:30:56 +03:00

635 lines
33 KiB
Python

from contextlib import contextmanager
import hashlib
import json
import os
from pathlib import Path
import stat
import subprocess
import sys
import tempfile
import time
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / 'app'
sys.path.insert(0, str(APP_DIR))
import runtime_security
@contextmanager
def isolated_endpoint_authority(directory):
root = os.path.join(runtime_security.canonical_path(directory), 'endpoint-authority')
namespace = hashlib.sha256(os.fsencode(root)).hexdigest()
mutex_name = runtime_security.cluster_endpoint_mutex_name
# Relocate only authority names; real private-file locks and Windows mutexes remain.
with mock.patch.object(runtime_security, '_cluster_endpoint_lock_root', return_value=root), \
mock.patch.object(runtime_security, 'cluster_endpoint_mutex_name', side_effect=lambda *args, **kwargs: (
mutex_name(*args, **kwargs) + '.fixture-' + namespace
)):
yield root
class RuntimeSecurityTests(unittest.TestCase):
def test_atomic_directory_publication_is_nonreplacing_and_preserves_tree(self):
with tempfile.TemporaryDirectory() as temp_dir:
source_parent = os.path.join(temp_dir, 'active')
destination_parent = os.path.join(temp_dir, 'abandoned')
os.makedirs(source_parent)
os.makedirs(destination_parent)
source = os.path.join(source_parent, 'assignment')
destination = os.path.join(destination_parent, 'assignment')
os.makedirs(source)
Path(source, 'proof.txt').write_text('proof', encoding='ascii')
runtime_security.durable_publish_directory(source, destination)
self.assertFalse(os.path.exists(source))
self.assertEqual(
Path(destination, 'proof.txt').read_text(encoding='ascii'),
'proof',
)
replacement = os.path.join(source_parent, 'assignment')
os.makedirs(replacement)
with self.assertRaises(FileExistsError):
runtime_security.durable_publish_directory(
replacement, destination,
)
self.assertTrue(os.path.isdir(replacement))
@unittest.skipUnless(os.name != 'nt' and os.geteuid() == 0, 'requires POSIX root')
def test_stable_root_file_requires_public_immutable_metadata(self):
with tempfile.TemporaryDirectory() as temp_dir:
manifest_root = os.path.join(temp_dir, 'manifests')
package_root = os.path.join(manifest_root, 'packages')
os.makedirs(package_root)
path = os.path.join(package_root, 'worker-package.json')
with open(path, 'wb') as handle:
handle.write(b'fixture')
os.chmod(path, 0o644)
self.assertEqual(
runtime_security.read_stable_root_file(path, 7, manifest_root),
b'fixture',
)
os.chmod(path, 0o600)
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.read_stable_root_file(path, 7, manifest_root)
os.chmod(path, 0o644)
linked = os.path.join(temp_dir, 'linked.json')
os.link(path, linked)
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.read_stable_root_file(path, 7, manifest_root)
os.unlink(linked)
os.chmod(package_root, 0o777)
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.read_stable_root_file(path, 7, manifest_root)
def test_discovery_preflight_keeps_code_authority_without_scanner_native_tools(self):
config = {
'global': {
'project_dir': 'project',
'trufflehog_path': 'trufflehog-fixture',
'trufflehog_config': 'detectors.yaml',
'secrets_file': 'secrets.yaml',
'proxy_file': 'proxies.txt',
},
'sources': {
'gitlab': {'trufflehog_config': 'gitlab-detectors.yaml'},
},
}
with mock.patch.object(runtime_security, 'require_private_file') as private_file, \
mock.patch.object(runtime_security, 'require_private_directory'), \
mock.patch('lifecycle_authority.manifest_authority_paths',
return_value=['project/authority.py']) as manifest, \
mock.patch('lifecycle_authority.resolve_manifest_executable') as resolve, \
mock.patch.object(runtime_security, 'require_trusted_native_executable') as native:
self.assertTrue(runtime_security.preflight_lifecycle_paths(
'config.yaml', config, authority_profile='discovery-producer',
))
manifest.assert_called_once_with(
'project', 'trufflehog-fixture', policy_paths=[],
existing_only=True, include_executables=False,
)
resolve.assert_not_called()
native.assert_not_called()
private_file.assert_any_call('config.yaml')
private_file.assert_any_call('project/authority.py')
self.assertNotIn(mock.call('detectors.yaml'), private_file.call_args_list)
self.assertNotIn(mock.call('gitlab-detectors.yaml'), private_file.call_args_list)
def test_unknown_lifecycle_preflight_profile_fails_closed(self):
with self.assertRaisesRegex(runtime_security.PrivateFileError, 'unsupported'):
runtime_security.preflight_lifecycle_paths(
'config.yaml', {}, authority_profile='arbitrary',
)
def test_private_json_extended_bound_is_explicit_and_capped(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'private')
runtime_security.ensure_private_directory(directory, reject_reparse=True)
path = os.path.join(directory, 'manifest.json')
value = {'payload': 'x' * (runtime_security.MAX_PRIVATE_JSON_BYTES + 1)}
with self.assertRaisesRegex(ValueError, 'too large'):
runtime_security.atomic_write_private_json(path, value)
runtime_security.atomic_write_private_json(
path, value, max_bytes=runtime_security.MAX_EXTENDED_PRIVATE_JSON_BYTES,
)
self.assertEqual(
runtime_security.read_private_json(
path, max_bytes=runtime_security.MAX_EXTENDED_PRIVATE_JSON_BYTES,
),
value,
)
with self.assertRaisesRegex(ValueError, 'bound is invalid'):
runtime_security.atomic_write_private_json(
path, value,
max_bytes=runtime_security.MAX_EXTENDED_PRIVATE_JSON_BYTES + 1,
)
def test_cluster_authority_create_parent_hardens_existing_directory(self):
with tempfile.TemporaryDirectory() as temp_dir, isolated_endpoint_authority(temp_dir) as endpoint_root:
runtime_security.ensure_private_directory(temp_dir, reject_reparse=True)
runtime = os.path.join(temp_dir, 'runtime')
postgres = os.path.join(runtime, 'postgres')
os.makedirs(postgres)
if os.name != 'nt':
os.chmod(postgres, 0o755)
self.assertFalse(runtime_security.private_directory_ready(postgres))
config = {'global': {'runtime_dir': runtime}}
lock = runtime_security.ClusterAuthorityLock(
config,
create_parent=True,
endpoint_dsn='postgresql://truf:fixture@127.0.0.1:15432/truf',
)
self.assertEqual(os.path.dirname(lock.endpoint_path), endpoint_root)
with mock.patch.object(runtime_security, 'harden_private_directory',
wraps=runtime_security.harden_private_directory) as harden:
try:
lock.acquire()
self.assertTrue(lock.acquired)
finally:
lock.release()
harden.assert_any_call(runtime_security.canonical_path(postgres))
self.assertFalse(lock.acquired)
self.assertTrue(runtime_security.private_directory_ready(postgres))
if os.name != 'nt':
self.assertTrue(runtime_security.private_directory_ready(endpoint_root))
def test_directory_and_file_exact_private_allowlists_verify(self):
with tempfile.TemporaryDirectory() as temp_dir:
directory = os.path.join(temp_dir, 'private')
runtime_security.ensure_private_directory(directory, reject_reparse=True)
path = os.path.join(directory, 'secret.txt')
Path(path).write_text('secret', encoding='ascii')
runtime_security.harden_private_file(path)
self.assertTrue(runtime_security.private_directory_ready(directory))
self.assertTrue(runtime_security.private_file_ready(path))
if os.name == 'nt':
sddl = runtime_security._windows_private_sddl(directory).upper()
self.assertEqual(sddl.count('(A;OICI;FA;'), 3)
else:
self.assertEqual(stat.S_IMODE(os.stat(directory).st_mode), 0o700)
self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o600)
def test_hardening_rejects_reparse_parent_without_touching_external_file(self):
with tempfile.TemporaryDirectory() as temp_dir:
external = os.path.join(temp_dir, 'external')
os.makedirs(external)
target = os.path.join(external, 'target.txt')
Path(target).write_text('external', encoding='ascii')
before = stat.S_IMODE(os.stat(target).st_mode)
link = os.path.join(temp_dir, 'linked')
try:
os.symlink(external, link, target_is_directory=True)
except (OSError, NotImplementedError):
self.skipTest('directory symlink creation is unavailable')
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.harden_private_file(os.path.join(link, 'target.txt'))
self.assertEqual(stat.S_IMODE(os.stat(target).st_mode), before)
def test_sensitive_startup_paths_fail_instead_of_repairing_existing_public_directory(self):
if os.name == 'nt':
self.skipTest('portable public-mode fixture is POSIX-specific')
with tempfile.TemporaryDirectory() as temp_dir:
public = os.path.join(temp_dir, 'runtime')
os.makedirs(public, mode=0o755)
os.chmod(public, 0o755)
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.require_sensitive_runtime_paths({'runtime_dir': public}, create=True)
def test_sensitive_file_accepts_protected_root_owned_parent(self):
details = mock.Mock(
st_mode=stat.S_IFDIR | 0o755,
st_uid=0,
st_gid=0,
)
with mock.patch.object(runtime_security.os, 'name', 'posix'), \
mock.patch.object(runtime_security, 'reject_reparse_components',
return_value='/data/config'), \
mock.patch.object(runtime_security, 'private_directory_ready',
return_value=False), \
mock.patch.object(runtime_security.os, 'stat', return_value=details), \
mock.patch.object(runtime_security.os, 'geteuid', return_value=10001,
create=True), \
mock.patch.object(runtime_security.os, 'getegid', return_value=10001,
create=True), \
mock.patch.object(runtime_security.os, 'getgroups', return_value=[],
create=True):
self.assertEqual(
runtime_security.require_protected_sensitive_file_parent('/data/config'),
'/data/config',
)
@mock.patch.object(runtime_security.os, 'name', 'posix')
@mock.patch.object(runtime_security, 'private_directory_ready', return_value=False)
@mock.patch.object(runtime_security.os, 'geteuid', return_value=10001, create=True)
@mock.patch.object(runtime_security.os, 'getegid', return_value=10001, create=True)
@mock.patch.object(runtime_security.os, 'getgroups', return_value=[], create=True)
def test_sensitive_file_rejects_unprotected_root_parent(
self, _groups, _egid, _euid, _private,
):
fixtures = {
'group-writable': (0, 0, 0o775),
'other-writable': (0, 0, 0o757),
'inaccessible': (0, 0, 0o750),
'runtime-owned-public': (10001, 10001, 0o755),
}
for name, (uid, gid, mode) in fixtures.items():
with self.subTest(name=name), \
mock.patch.object(runtime_security, 'reject_reparse_components',
return_value='/data/config'), \
mock.patch.object(runtime_security.os, 'stat', return_value=mock.Mock(
st_mode=stat.S_IFDIR | mode, st_uid=uid, st_gid=gid,
)):
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.require_protected_sensitive_file_parent('/data/config')
def test_sensitive_file_parent_rejects_link_before_metadata_check(self):
with mock.patch.object(
runtime_security, 'reject_reparse_components',
side_effect=runtime_security.PrivateFileError('link'),
), mock.patch.object(runtime_security.os, 'stat') as inspect:
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.require_protected_sensitive_file_parent('/data/config')
inspect.assert_not_called()
@mock.patch.object(runtime_security.os, 'name', 'posix')
def test_private_file_rejects_permissive_or_wrong_owner(self):
for name, uid, mode in (
('permissive', 10001, 0o640),
('wrong-owner', 0, 0o600),
):
with self.subTest(name=name), \
mock.patch.object(runtime_security, 'reject_reparse_components',
return_value='/data/config/secrets.yaml'), \
mock.patch.object(runtime_security.os, 'stat', return_value=mock.Mock(
st_mode=stat.S_IFREG | mode, st_uid=uid, st_gid=10001,
)), \
mock.patch.object(runtime_security.os, 'geteuid', return_value=10001,
create=True):
self.assertFalse(
runtime_security.private_file_ready('/data/config/secrets.yaml')
)
def test_windows_acl_verification_requires_actual_owner_sid_not_owner_rights(self):
with tempfile.TemporaryDirectory() as temp_dir, \
mock.patch.object(runtime_security.os, 'name', 'nt'), \
mock.patch.object(runtime_security, 'reject_reparse_components'), \
mock.patch.object(runtime_security, '_windows_current_user_sid', return_value='S-1-5-21-100'):
valid = (
'O:S-1-5-21-100D:P'
'(A;OICI;FA;;;S-1-5-21-100)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)'
)
with mock.patch.object(runtime_security, '_windows_private_sddl', return_value=valid):
self.assertTrue(runtime_security._private_acl_ready(temp_dir, directory=True))
generic_owner_rights = 'O:S-1-5-21-100D:P(A;OICI;FA;;;OW)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)'
with mock.patch.object(runtime_security, '_windows_private_sddl', return_value=generic_owner_rights):
self.assertFalse(runtime_security._private_acl_ready(temp_dir, directory=True))
foreign_owner = valid.replace('O:S-1-5-21-100', 'O:S-1-5-21-999', 1)
with mock.patch.object(runtime_security, '_windows_private_sddl', return_value=foreign_owner):
self.assertFalse(runtime_security._private_acl_ready(temp_dir, directory=True))
def test_lifecycle_preflight_rejects_reparse_work_path_without_mutation(self):
with tempfile.TemporaryDirectory() as temp_dir:
runtime_security.ensure_private_directory(temp_dir, reject_reparse=True)
project = os.path.join(temp_dir, 'project')
runtime = os.path.join(temp_dir, 'runtime')
external = os.path.join(temp_dir, 'external')
for path in (
project, runtime, external,
os.path.join(runtime, 'control'), os.path.join(runtime, 'logs'),
os.path.join(runtime, 'results'), os.path.join(runtime, 'queues'),
os.path.join(runtime, 'state'), os.path.join(runtime, 'keychecks'),
os.path.join(runtime, 'result_spool'), os.path.join(runtime, 'postgres'),
):
runtime_security.ensure_private_directory(path, reject_reparse=True)
linked_work = os.path.join(temp_dir, 'work')
try:
os.symlink(external, linked_work, target_is_directory=True)
except (OSError, NotImplementedError):
self.skipTest('directory symlink creation is unavailable')
config_path = os.path.join(project, 'config.yaml')
Path(config_path).write_text('{}\n', encoding='ascii')
runtime_security.harden_private_file(config_path)
config = {
'global': {
'root_dir': temp_dir,
'project_dir': project,
'runtime_dir': runtime,
'control_dir': os.path.join(runtime, 'control'),
'log_dir': os.path.join(runtime, 'logs'),
'work_dir': linked_work,
'results_dir': os.path.join(runtime, 'results'),
'queue_dir': os.path.join(runtime, 'queues'),
'state_dir': os.path.join(runtime, 'state'),
'keycheck_dir': os.path.join(runtime, 'keychecks'),
'result_spool_dir': os.path.join(runtime, 'result_spool'),
},
'supervisor': {
'control_dir': os.path.join(runtime, 'control'),
'log_dir': os.path.join(runtime, 'logs'),
},
}
before = os.stat(external, follow_symlinks=False)
with self.assertRaisesRegex(runtime_security.PrivateFileError, 'offline hardening'):
runtime_security.preflight_lifecycle_paths(config_path, config)
after = os.stat(external, follow_symlinks=False)
self.assertEqual((before.st_mode, before.st_mtime_ns), (after.st_mode, after.st_mtime_ns))
def test_external_postgres_data_is_bound_to_preflight_and_cluster_lock(self):
with tempfile.TemporaryDirectory() as temp_dir, isolated_endpoint_authority(temp_dir) as endpoint_root:
runtime_security.ensure_private_directory(temp_dir, reject_reparse=True)
project = os.path.join(temp_dir, 'project')
runtime = os.path.join(temp_dir, 'runtime')
postgres_dir = os.path.join(runtime, 'postgres')
external_data = os.path.join(temp_dir, 'external-data')
for path in (project, runtime, postgres_dir, external_data, endpoint_root):
runtime_security.ensure_private_directory(path, reject_reparse=True)
config_path = os.path.join(project, 'config.yaml')
code_path = os.path.join(project, 'authority.py')
policy_path = os.path.join(project, 'policy.yaml')
for path in (config_path, code_path, policy_path):
Path(path).write_text('{}\n', encoding='ascii')
runtime_security.harden_private_file(path)
tools = {name: os.path.join(temp_dir, 'fixture-' + name) for name in ('trufflehog', 'git')}
config = {'global': {
'root_dir': temp_dir,
'project_dir': project,
'runtime_dir': runtime,
'postgres_data_dir': external_data,
'trufflehog_path': tools['trufflehog'],
'trufflehog_config': policy_path,
}}
def resolve(value, *, name, app_dir):
self.assertEqual(app_dir, project)
self.assertEqual(value, tools[name] if name == 'trufflehog' else None)
return tools[name]
rejected_tool = None
def verify_native(path):
self.assertIn(path, tools.values())
if path == rejected_tool:
raise runtime_security.PrivateFileError('fixture native executable is untrusted')
return path
with mock.patch('lifecycle_authority.manifest_authority_paths',
return_value=[code_path, policy_path]) as manifest, \
mock.patch('lifecycle_authority.resolve_manifest_executable', side_effect=resolve) as resolved, \
mock.patch.object(runtime_security, 'require_trusted_native_executable', side_effect=verify_native) as native, \
mock.patch.object(runtime_security, 'require_private_directory',
wraps=runtime_security.require_private_directory) as directories, \
mock.patch.object(runtime_security, 'require_private_file',
wraps=runtime_security.require_private_file) as private_files, \
mock.patch.object(runtime_security, 'harden_private_directory',
side_effect=AssertionError('preflight must not repair directories')), \
mock.patch.object(runtime_security, 'harden_private_file',
side_effect=AssertionError('preflight must not repair files')):
self.assertTrue(runtime_security.preflight_lifecycle_paths(config_path, config))
manifest.assert_called_once_with(
project, tools['trufflehog'], policy_paths=[policy_path],
existing_only=True, include_executables=False,
)
self.assertEqual(resolved.call_args_list, [
mock.call(tools['trufflehog'], name='trufflehog', app_dir=project),
mock.call(None, name='git', app_dir=project),
])
self.assertEqual(native.call_args_list, [mock.call(path) for path in tools.values()])
directories.assert_any_call(runtime_security.canonical_path(external_data), create=False)
if os.name != 'nt':
directories.assert_any_call(endpoint_root, create=False)
for path in (config_path, code_path, policy_path):
private_files.assert_any_call(path)
for rejected_tool in tools.values():
with self.subTest(untrusted_tool=os.path.basename(rejected_tool)), \
self.assertRaisesRegex(runtime_security.PrivateFileError, 'native executable is untrusted'):
runtime_security.preflight_lifecycle_paths(config_path, config)
rejected_tool = None
native.reset_mock()
Path(external_data).rmdir()
Path(external_data).write_text('not a directory\n', encoding='ascii')
with self.assertRaises(runtime_security.PrivateFileError):
runtime_security.preflight_lifecycle_paths(config_path, config)
native.assert_not_called()
self.assertEqual(
runtime_security.canonical_cluster_data_directory(config),
runtime_security.canonical_path(external_data),
)
self.assertEqual(
os.path.dirname(runtime_security.cluster_authority_lock_path(config)),
runtime_security.canonical_path(postgres_dir),
)
self.assertNotEqual(
runtime_security.cluster_authority_lock_path(config),
runtime_security.cluster_authority_lock_path({
'global': {'runtime_dir': runtime},
}),
)
def test_postgres_data_directory_rejects_filesystem_root(self):
config = {'global': {
'runtime_dir': os.path.join(os.getcwd(), 'runtime'),
'postgres_data_dir': os.path.abspath(os.sep),
}}
with self.assertRaisesRegex(runtime_security.PrivateFileError, 'volume root|filesystem'):
runtime_security.canonical_cluster_data_directory(config)
def test_postgres_data_directory_rejects_reparse_component(self):
with tempfile.TemporaryDirectory() as temp_dir:
external = os.path.join(temp_dir, 'external')
os.makedirs(external)
linked = os.path.join(temp_dir, 'linked-data')
try:
os.symlink(external, linked, target_is_directory=True)
except (OSError, NotImplementedError):
self.skipTest('directory symlink creation is unavailable')
config = {'global': {
'runtime_dir': os.path.join(temp_dir, 'runtime'),
'postgres_data_dir': linked,
}}
with self.assertRaisesRegex(runtime_security.PrivateFileError, 'reparse point|link'):
runtime_security.canonical_cluster_data_directory(config)
def test_cluster_authority_lock_contends_across_alternate_configs_and_processes(self):
with tempfile.TemporaryDirectory() as temp_dir, isolated_endpoint_authority(temp_dir) as endpoint_root:
runtime_security.ensure_private_directory(temp_dir, reject_reparse=True)
runtime_a = os.path.join(temp_dir, 'cluster-a')
runtime_b = os.path.join(temp_dir, 'cluster-b')
for runtime in (runtime_a, runtime_b):
runtime_security.ensure_private_directory(runtime, reject_reparse=True)
runtime_security.ensure_private_directory(os.path.join(runtime, 'postgres'), reject_reparse=True)
first = {
'global': {
'runtime_dir': runtime_a,
'control_dir': os.path.join(temp_dir, 'control-one'),
'database_url': 'postgresql://cluster:one@127.0.0.1:15432/cluster_a',
},
'supervisor': {'control_port': 11001},
}
alternate = {
'global': {
'runtime_dir': runtime_a,
'control_dir': os.path.join(temp_dir, 'control-two'),
'database_url': 'postgresql://cluster:different@localhost:15432/cluster_a',
},
'supervisor': {'control_port': 22002},
}
independent = {
'global': {
'runtime_dir': runtime_b,
'control_dir': os.path.join(temp_dir, 'control-three'),
'database_url': 'postgresql://cluster:two@127.0.0.1:25432/cluster_b',
},
'supervisor': {'control_port': 33003},
}
same_endpoint_other_data = {
'global': {
'runtime_dir': runtime_b,
'control_dir': os.path.join(temp_dir, 'control-four'),
'database_url': 'postgresql://cluster:other-password@127.0.0.1:15432/cluster_a',
},
'supervisor': {'control_port': 44004},
}
self.assertEqual(
runtime_security.cluster_authority_lock_path(first),
runtime_security.cluster_authority_lock_path(alternate),
)
self.assertNotEqual(
runtime_security.cluster_authority_lock_path(first),
runtime_security.cluster_authority_lock_path(independent),
)
self.assertNotEqual(
runtime_security.cluster_authority_lock_path(first),
runtime_security.cluster_authority_lock_path(same_endpoint_other_data),
)
self.assertEqual(
runtime_security.cluster_endpoint_authority_lock_path(first, endpoint_dsn=first['global']['database_url']),
runtime_security.cluster_endpoint_authority_lock_path(same_endpoint_other_data, endpoint_dsn=same_endpoint_other_data['global']['database_url']),
)
self.assertNotEqual(
runtime_security.cluster_endpoint_authority_lock_path(first, endpoint_dsn=first['global']['database_url']),
runtime_security.cluster_endpoint_authority_lock_path(independent, endpoint_dsn=independent['global']['database_url']),
)
fixture_code = (
'import json,os,runpy,sys\n'
'fixture=runpy.run_path(sys.argv[1])\n'
'cfg=json.loads(sys.argv[2])\n'
'with fixture["isolated_endpoint_authority"](sys.argv[3]) as root:\n'
' lock=fixture["runtime_security"].ClusterAuthorityLock(cfg,endpoint_dsn=cfg["global"]["database_url"])\n'
' assert os.path.dirname(lock.endpoint_path)==root\n'
)
holder_code = fixture_code + (
' with lock:\n'
' with open(sys.argv[4], "x", encoding="ascii") as ready: ready.write("READY")\n'
' sys.stdin.readline()\n'
)
contender_code = fixture_code + (
' try: lock.acquire()\n'
' except BlockingIOError: raise SystemExit(3)\n'
' lock.release()\n'
)
command = [sys.executable, '-I', '-S', '-B', '-c']
fixture_path = str(Path(__file__).resolve())
ready = Path(temp_dir) / 'holder.ready'
holder = subprocess.Popen(
command + [holder_code, fixture_path, json.dumps(first), temp_dir, str(ready)],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
try:
deadline = time.monotonic() + 10
while not ready.exists() and holder.poll() is None and time.monotonic() < deadline:
time.sleep(0.02)
self.assertTrue(ready.exists(), 'isolated lock holder did not become ready')
blocked = subprocess.run(
command + [contender_code, fixture_path, json.dumps(alternate), temp_dir],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=15,
check=False,
)
self.assertEqual(blocked.returncode, 3, blocked.stderr)
endpoint_blocked = subprocess.run(
command + [contender_code, fixture_path, json.dumps(same_endpoint_other_data), temp_dir],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=15,
check=False,
)
self.assertEqual(endpoint_blocked.returncode, 3, endpoint_blocked.stderr)
coexisting = subprocess.run(
command + [contender_code, fixture_path, json.dumps(independent), temp_dir],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=15,
check=False,
)
self.assertEqual(coexisting.returncode, 0, coexisting.stderr)
finally:
try:
holder.communicate('\n', timeout=10)
except subprocess.TimeoutExpired:
holder.kill()
holder.communicate(timeout=10)
self.assertEqual(holder.returncode, 0)
maintenance = runtime_security.ClusterAuthorityLock(
alternate, endpoint_dsn=alternate['global']['database_url'],
)
self.assertEqual(os.path.dirname(maintenance.endpoint_path), endpoint_root)
try:
maintenance.acquire()
self.assertTrue(maintenance.acquired)
runtime_blocked = subprocess.run(
command + [contender_code, fixture_path, json.dumps(first), temp_dir],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=15,
check=False,
)
self.assertEqual(runtime_blocked.returncode, 3, runtime_blocked.stderr)
finally:
maintenance.release()
if __name__ == '__main__':
unittest.main()