Initial server source import
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import stat
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEPLOY = ROOT / 'deploy' / 'host-agent'
|
||||
INSTALLER_PATH = DEPLOY / 'truf_host_agent_install.py'
|
||||
SPEC = importlib.util.spec_from_file_location('truf_host_agent_install', INSTALLER_PATH)
|
||||
installer = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(installer)
|
||||
|
||||
|
||||
class HostAgentDeployTests(unittest.TestCase):
|
||||
def test_secure_executable_accepts_root_owned_usr_bin_symlink(self):
|
||||
link = mock.Mock(st_uid=0, st_mode=stat.S_IFLNK | 0o777)
|
||||
target = mock.Mock(st_uid=0, st_mode=stat.S_IFREG | 0o755)
|
||||
parent = mock.Mock(st_uid=0, st_mode=stat.S_IFDIR | 0o755)
|
||||
with mock.patch.object(installer.os, 'lstat', return_value=link), \
|
||||
mock.patch.object(installer.os.path, 'realpath', return_value='/usr/bin/python3.14'), \
|
||||
mock.patch.object(installer.os, 'stat', side_effect=(target, parent)):
|
||||
installer._secure_executable('/usr/bin/python3')
|
||||
|
||||
def test_secure_executable_rejects_symlink_outside_usr_bin(self):
|
||||
link = mock.Mock(st_uid=0, st_mode=stat.S_IFLNK | 0o777)
|
||||
target = mock.Mock(st_uid=0, st_mode=stat.S_IFREG | 0o755)
|
||||
parent = mock.Mock(st_uid=0, st_mode=stat.S_IFDIR | 0o755)
|
||||
with mock.patch.object(installer.os, 'lstat', return_value=link), \
|
||||
mock.patch.object(installer.os.path, 'realpath', return_value='/tmp/python3'), \
|
||||
mock.patch.object(installer.os, 'stat', side_effect=(target, parent)), \
|
||||
self.assertRaises(installer.InstallError):
|
||||
installer._secure_executable('/usr/bin/python3')
|
||||
|
||||
def test_socket_unit_exposes_only_fixed_runtime_socket(self):
|
||||
unit = (DEPLOY / 'truf-host-agent.socket').read_text(encoding='ascii')
|
||||
self.assertIn('ListenStream=/run/truf/host-agent.sock\n', unit)
|
||||
self.assertIn('SocketUser=root\n', unit)
|
||||
self.assertIn('SocketGroup=truf-runtime\n', unit)
|
||||
self.assertIn('SocketMode=0660\n', unit)
|
||||
self.assertIn('Accept=no\n', unit)
|
||||
self.assertNotIn('%', unit)
|
||||
self.assertNotIn('Environment', unit)
|
||||
|
||||
def test_service_has_fixed_entrypoints_and_narrow_host_authority(self):
|
||||
unit = (DEPLOY / 'truf-host-agent.service').read_text(encoding='ascii')
|
||||
self.assertIn(
|
||||
'ExecStartPre=/usr/bin/python3 -I -B '
|
||||
'/usr/lib/truf-host-agent/truf_host_agent_install.py validate\n',
|
||||
unit,
|
||||
)
|
||||
self.assertIn(
|
||||
'ExecStart=/usr/bin/python3 -I -B '
|
||||
'/usr/lib/truf-host-agent/truf_host_agent.py\n',
|
||||
unit,
|
||||
)
|
||||
for directive in (
|
||||
'User=root', 'NoNewPrivileges=yes', 'PrivateNetwork=yes',
|
||||
'ProtectSystem=strict', 'RestrictAddressFamilies=AF_UNIX',
|
||||
'ReadWritePaths=/etc/truf/runtime',
|
||||
'ReadWritePaths=/var/lib/truf/host-agent',
|
||||
'ReadWritePaths=/run/truf-host-agent',
|
||||
'ReadWritePaths=/run/docker.sock',
|
||||
'ReadOnlyPaths=/var/lib/truf/runtime-document-candidates',
|
||||
'Restart=no',
|
||||
):
|
||||
self.assertIn(directive + '\n', unit)
|
||||
self.assertNotIn('Environment=', unit)
|
||||
self.assertNotIn('/bin/sh', unit)
|
||||
self.assertIn('StandardOutput=null\n', unit)
|
||||
self.assertIn('StandardError=journal\n', unit)
|
||||
|
||||
def test_tmpfiles_declares_only_fixed_directories_and_modes(self):
|
||||
lines = (DEPLOY / 'truf-host-agent.conf').read_text(
|
||||
encoding='ascii',
|
||||
).splitlines()
|
||||
self.assertEqual(lines, [
|
||||
'd /etc/truf/runtime 0755 root root -',
|
||||
'd /etc/truf/worker-packages 0755 root root -',
|
||||
'd /var/lib/truf/runtime-document-candidates 0700 10001 10001 -',
|
||||
'd /var/lib/truf/host-agent 0700 root root -',
|
||||
'd /var/lib/truf/host-agent/backups 0700 root root -',
|
||||
'd /var/lib/truf/host-agent/operations 0700 root root -',
|
||||
'd /var/lib/truf/host-agent/results 0750 root 10001 -',
|
||||
'd /run/truf-postgres 0700 10001 10001 -',
|
||||
])
|
||||
|
||||
|
||||
class HostAgentInstallerTests(unittest.TestCase):
|
||||
def test_profile_is_exact_root_owned_policy_with_standalone_default(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
missing = Path(temporary) / 'missing'
|
||||
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', missing):
|
||||
self.assertIs(installer._deployment_profile(), installer.STANDALONE_PROFILE)
|
||||
|
||||
selected = Path(temporary) / 'profile'
|
||||
selected.write_text('shared-host-edge-v1\n', encoding='ascii')
|
||||
selected.chmod(0o444)
|
||||
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', selected):
|
||||
self.assertIs(installer._deployment_profile(), installer.SHARED_HOST_PROFILE)
|
||||
|
||||
selected.chmod(0o666)
|
||||
with mock.patch.object(installer, 'DEPLOYMENT_PROFILE', selected), \
|
||||
self.assertRaises(installer.InstallError):
|
||||
installer._deployment_profile()
|
||||
|
||||
def test_main_accepts_only_fixed_install_or_validate_action(self):
|
||||
with mock.patch.object(sys, 'argv', ['installer', 'validate']), \
|
||||
mock.patch.object(installer, 'validate') as validate:
|
||||
self.assertEqual(installer.main(), 0)
|
||||
validate.assert_called_once_with()
|
||||
|
||||
with mock.patch.object(sys, 'argv', ['installer', 'install']), \
|
||||
mock.patch.object(installer, 'install') as install:
|
||||
self.assertEqual(installer.main(), 0)
|
||||
install.assert_called_once_with()
|
||||
|
||||
for arguments in ([], ['restart'], ['validate', 'extra']):
|
||||
with self.subTest(arguments=arguments), \
|
||||
mock.patch.object(sys, 'argv', ['installer', *arguments]):
|
||||
with self.assertRaises(installer.InstallError):
|
||||
installer.main()
|
||||
|
||||
def test_install_uses_fixed_sources_and_never_replaces_active_documents(self):
|
||||
reads = []
|
||||
|
||||
def read_source(path, maximum=installer.MAX_COPY_BYTES):
|
||||
reads.append((path, maximum))
|
||||
return str(path).encode('ascii')
|
||||
|
||||
with mock.patch.object(installer.sys, 'platform', 'linux'), \
|
||||
mock.patch.object(installer.os, 'geteuid', return_value=0, create=True), \
|
||||
mock.patch.object(installer, '_ensure_runtime_group'), \
|
||||
mock.patch.object(installer, '_unit_active', return_value=True) as active, \
|
||||
mock.patch.object(installer, '_ensure_install_root'), \
|
||||
mock.patch.object(installer, '_read_source', side_effect=read_source), \
|
||||
mock.patch.object(installer, '_write') as write, \
|
||||
mock.patch.object(installer, '_run') as run, \
|
||||
mock.patch.object(installer, 'validate') as validate, \
|
||||
mock.patch.object(installer, '_validate_agent_socket') as socket:
|
||||
installer.install()
|
||||
|
||||
self.assertEqual([call.args[0] for call in write.call_args_list], [
|
||||
installer.INSTALL_ROOT / 'truf_host_agent.py',
|
||||
installer.INSTALL_ROOT / 'truf_host_agent_install.py',
|
||||
installer.SYSTEMD / 'truf-host-agent.socket',
|
||||
installer.SYSTEMD / 'truf-host-agent.service',
|
||||
installer.TMPFILES,
|
||||
installer.ACTIVE / 'config.yaml',
|
||||
installer.ACTIVE / 'secrets.yaml',
|
||||
])
|
||||
self.assertTrue(all(call.kwargs['replace'] for call in write.call_args_list[:5]))
|
||||
self.assertTrue(all(not call.kwargs['replace'] for call in write.call_args_list[5:]))
|
||||
self.assertEqual(reads, [
|
||||
(installer.DEPLOY / 'truf_host_agent.py', installer.MAX_COPY_BYTES),
|
||||
(installer.DEPLOY / 'truf_host_agent_install.py', installer.MAX_COPY_BYTES),
|
||||
(installer.DEPLOY / 'truf-host-agent.socket', installer.MAX_COPY_BYTES),
|
||||
(installer.DEPLOY / 'truf-host-agent.service', installer.MAX_COPY_BYTES),
|
||||
(installer.DEPLOY / 'truf-host-agent.conf', installer.MAX_COPY_BYTES),
|
||||
(installer.PROJECT / 'app/config.linux.yaml', installer.MAX_COPY_BYTES),
|
||||
])
|
||||
self.assertEqual([call.args[0] for call in run.call_args_list], [
|
||||
('/usr/bin/systemctl', 'stop', 'truf-host-agent.socket'),
|
||||
('/usr/bin/systemctl', 'stop', 'truf-host-agent.service'),
|
||||
('/usr/bin/systemd-tmpfiles', '--create', str(installer.TMPFILES)),
|
||||
('/usr/bin/systemctl', 'daemon-reload'),
|
||||
('/usr/bin/systemctl', 'enable', 'truf-host-agent.socket'),
|
||||
('/usr/bin/systemctl', 'restart', 'truf-host-agent.socket'),
|
||||
])
|
||||
self.assertEqual(
|
||||
[call.args[0] for call in active.call_args_list],
|
||||
list(installer.UNITS),
|
||||
)
|
||||
validate.assert_called_once_with(require_socket=False)
|
||||
socket.assert_called_once_with()
|
||||
|
||||
def test_compose_projection_requires_exact_runtime_mounts(self):
|
||||
mounts = []
|
||||
for kind, source, target, read_only, create_host_path in installer.EXPECTED_RUNTIME_MOUNTS:
|
||||
mount = {'type': kind, 'source': source, 'target': target}
|
||||
if read_only:
|
||||
mount['read_only'] = True
|
||||
if create_host_path is not None:
|
||||
mount['bind'] = {'create_host_path': create_host_path}
|
||||
mounts.append(mount)
|
||||
projection = {
|
||||
'name': 'truf-docker',
|
||||
'volumes': {'data': {'name': 'truf-docker_data'}},
|
||||
'services': {
|
||||
'runtime': {
|
||||
'network_mode': None,
|
||||
'ports': installer.STANDALONE_PROFILE['runtime_ports'],
|
||||
'cpus': 2.0,
|
||||
'mem_limit': str(6 * 1024 ** 3),
|
||||
'volumes': mounts,
|
||||
},
|
||||
'edge': {
|
||||
'network_mode': 'service:runtime',
|
||||
'cap_add': ['NET_BIND_SERVICE'],
|
||||
'image': 'truf-local:edge',
|
||||
},
|
||||
},
|
||||
}
|
||||
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
|
||||
|
||||
normalized = json.loads(json.dumps(projection))
|
||||
for mount in normalized['services']['runtime']['volumes']:
|
||||
if mount['type'] == 'bind':
|
||||
mount['bind'] = {}
|
||||
installer._validate_compose_projection(json.dumps(normalized).encode('ascii'))
|
||||
|
||||
mounts[1]['source'] = '/etc/truf/substituted'
|
||||
with self.assertRaises(installer.InstallError):
|
||||
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
|
||||
|
||||
def test_compose_projection_accepts_only_exact_shared_host_profile(self):
|
||||
mounts = []
|
||||
for kind, source, target, read_only, create_host_path in installer.EXPECTED_RUNTIME_MOUNTS:
|
||||
mount = {'type': kind, 'source': source, 'target': target}
|
||||
if read_only:
|
||||
mount['read_only'] = True
|
||||
if create_host_path is not None:
|
||||
mount['bind'] = {'create_host_path': create_host_path}
|
||||
mounts.append(mount)
|
||||
projection = {
|
||||
'name': 'truf-docker',
|
||||
'volumes': {
|
||||
'data': {'name': 'truf-remote-server-data', 'external': True},
|
||||
},
|
||||
'services': {
|
||||
'runtime': {
|
||||
'network_mode': 'host', 'ports': None, 'cpus': 0.9,
|
||||
'mem_limit': str(720 * 1024 ** 2), 'volumes': mounts,
|
||||
},
|
||||
'edge': {
|
||||
'network_mode': 'service:runtime', 'cap_add': None,
|
||||
'image': 'truf-local:edge',
|
||||
},
|
||||
},
|
||||
}
|
||||
payload = json.dumps(projection).encode('ascii')
|
||||
installer._validate_compose_projection(payload, installer.SHARED_HOST_PROFILE)
|
||||
projection['services']['runtime']['ports'] = [{
|
||||
'target': 443, 'published': '443', 'protocol': 'tcp', 'mode': 'host',
|
||||
}]
|
||||
with self.assertRaises(installer.InstallError):
|
||||
installer._validate_compose_projection(payload.replace(b'"ports": null', b'"ports": []'), installer.SHARED_HOST_PROFILE)
|
||||
|
||||
drifted = json.loads(payload)
|
||||
drifted['services']['runtime']['cpus'] = 2.0
|
||||
with self.assertRaises(installer.InstallError):
|
||||
installer._validate_compose_projection(
|
||||
json.dumps(drifted).encode('ascii'), installer.SHARED_HOST_PROFILE,
|
||||
)
|
||||
|
||||
mounts[1]['source'] = '/etc/truf/runtime'
|
||||
mounts[1]['bind']['create_host_path'] = True
|
||||
with self.assertRaises(installer.InstallError):
|
||||
installer._validate_compose_projection(json.dumps(projection).encode('ascii'))
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'POSIX durable file operations required')
|
||||
def test_write_does_not_replace_existing_active_file(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
parent = Path(temporary)
|
||||
target = parent / 'config.yaml'
|
||||
target.write_bytes(b'original')
|
||||
os.chmod(target, 0o600)
|
||||
with mock.patch.object(installer.os, 'fchown'):
|
||||
installer._write(
|
||||
target, b'candidate', uid=os.getuid(), gid=os.getgid(),
|
||||
mode=0o600, replace=False,
|
||||
)
|
||||
self.assertEqual(target.read_bytes(), b'original')
|
||||
self.assertFalse((parent / '.config.yaml.truf-install').exists())
|
||||
|
||||
with mock.patch.object(installer.os, 'fchown'):
|
||||
installer._write(
|
||||
target, b'replacement', uid=os.getuid(), gid=os.getgid(),
|
||||
mode=0o600, replace=True,
|
||||
)
|
||||
self.assertEqual(target.read_bytes(), b'replacement')
|
||||
self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o600)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user