83 lines
3.6 KiB
Python
83 lines
3.6 KiB
Python
import importlib.util
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
import contextlib
|
|
import io
|
|
import json
|
|
import sys
|
|
from types import SimpleNamespace
|
|
|
|
MODULE = Path(__file__).resolve().parents[1] / "deploy/start.py"
|
|
spec = importlib.util.spec_from_file_location("start_console", MODULE)
|
|
start = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(start)
|
|
|
|
|
|
class DeploymentPathsTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.base = Path(self.temp.name)
|
|
self.project = self.base / "project"
|
|
self.project.mkdir()
|
|
self.outside = self.base / "other-owner"
|
|
self.outside.mkdir()
|
|
|
|
def validator(self):
|
|
self.assertTrue(hasattr(start, "validate_runtime"), "runtime boundary validation is missing")
|
|
return start.validate_runtime
|
|
|
|
def link(self, target, destination):
|
|
try:
|
|
destination.symlink_to(target, target_is_directory=True)
|
|
except OSError:
|
|
self.skipTest("OS account cannot create a directory symlink")
|
|
|
|
def test_runtime_parent_link_cannot_escape_project(self):
|
|
validate = self.validator()
|
|
self.link(self.outside, self.project / "runtime")
|
|
before = self.outside.stat().st_mode
|
|
with self.assertRaises(ValueError):
|
|
validate(self.project)
|
|
self.assertEqual(self.outside.stat().st_mode, before)
|
|
self.assertEqual(list(self.outside.iterdir()), [])
|
|
|
|
def test_runtime_child_link_cannot_escape_project(self):
|
|
validate = self.validator()
|
|
(self.project / "runtime").mkdir()
|
|
self.link(self.outside, self.project / "runtime/data")
|
|
with self.assertRaises(ValueError):
|
|
validate(self.project)
|
|
self.assertEqual(list(self.outside.iterdir()), [])
|
|
|
|
def test_normal_runtime_is_resolved_without_writing(self):
|
|
validate = self.validator()
|
|
self.assertEqual(validate(self.project), self.project.resolve() / "runtime")
|
|
self.assertEqual(list(self.project.iterdir()), [])
|
|
|
|
def test_backend_is_only_on_proxy_loopback_namespace(self):
|
|
# Regress the reviewed direct-container-IP role-header bypass at the Docker boundary.
|
|
deploy = self.project / 'deploy'
|
|
deploy.mkdir()
|
|
(deploy/'lan-settings.json').write_text(json.dumps({
|
|
'address':'10.88.118.188','allowed_cidrs':['10.88.0.0/17','10.20.0.0/21'],'port':14440}))
|
|
calls = []
|
|
def docker(args, **kwargs):
|
|
calls.append(args)
|
|
absent = args[:3] in (['docker','container','inspect'], ['docker','network','inspect'])
|
|
return SimpleNamespace(returncode=1 if absent else 0, stdout='')
|
|
with mock.patch.object(sys, 'argv', ['start.py','--project-root',str(self.project)]), \
|
|
mock.patch.object(sys, 'path', [str(MODULE.parent)] + sys.path), \
|
|
mock.patch.object(start.subprocess, 'run', side_effect=docker), \
|
|
mock.patch.object(start.os, 'chown', create=True), \
|
|
mock.patch.object(start.socket, 'socket'), contextlib.redirect_stdout(io.StringIO()):
|
|
start.main()
|
|
backend = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console')
|
|
proxy = next(c for c in calls if '--name' in c and c[c.index('--name')+1]=='adamwen-test-console-proxy')
|
|
self.assertIn('RUNDECK_SERVER_ADDRESS=127.0.0.1', backend)
|
|
self.assertEqual(backend[backend.index('--network')+1], 'container:adamwen-test-console-proxy')
|
|
self.assertNotIn('--publish', backend)
|
|
self.assertLess(calls.index(proxy), calls.index(backend))
|