"""Unit tests for the fail-closed P12 RC isolation preflight.""" from __future__ import annotations import importlib.util import tempfile import unittest from pathlib import Path from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] SPEC = importlib.util.spec_from_file_location("rc_isolation_preflight", ROOT / "scripts" / "rc_isolation_preflight.py") assert SPEC is not None and SPEC.loader is not None preflight = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(preflight) PROJECT = "modelforge-validation-p12a" def compose_plan(root: Path) -> dict[str, object]: return { "services": { "api": { "networks": ["default"], "volumes": [ {"type": "volume", "source": "postgres-data", "target": "/data"}, {"type": "bind", "source": str(root / "config"), "target": "/app/config", "read_only": True}, ], "ports": [{"host_ip": "127.0.0.1", "published": "0", "target": 8000}], }, "node-agent": { "networks": ["default"], "volumes": [{"type": "volume", "source": "node-state", "target": "/data/state"}], }, }, "volumes": { "postgres-data": {"name": f"{PROJECT}_postgres-data"}, "node-state": {"name": f"{PROJECT}_node-state"}, }, "networks": {"default": {"name": f"{PROJECT}_default"}}, } class RcIsolationPreflightTests(unittest.TestCase): def plan_root(self, *, with_manifest: bool = True) -> tempfile.TemporaryDirectory[str]: temporary = tempfile.TemporaryDirectory() config = Path(temporary.name, "config") config.mkdir() if with_manifest: manifest = config / "models" / "initial-candidates.yaml" manifest.parent.mkdir() manifest.write_text("candidates: []\n", encoding="utf-8") self.addCleanup(temporary.cleanup) return temporary def validate( self, plan: dict[str, object], root: Path, *, ports: set[int] | None = None, names: set[str] | None = None, ) -> dict[str, object]: return preflight.validate_compose( plan, project=PROJECT, candidate_root=root, protected_ports=ports or set(), protected_resource_names=names or set(), ) def test_valid_validation_compose_plan_is_accepted(self) -> None: temporary = self.plan_root() root = Path(temporary.name) result = self.validate( compose_plan(root), root, ports={18000, 13000}, names={"modelforge-postgres-1"} ) self.assertEqual(result["project"], PROJECT) self.assertEqual(result["published_ports"], []) def test_project_validation_rejects_production_and_legacy_rc_names(self) -> None: for project in ( "modelforge", "modelforge-rc-p12a", "modelforge-validation-", "modelforge-validation-prod-", ): with self.subTest(project=project), self.assertRaises(preflight.IsolationError): preflight.validate_project(project) def test_missing_bind_source_is_rejected(self) -> None: temporary = self.plan_root(with_manifest=False) root = Path(temporary.name) plan = compose_plan(root) (root / "config").rmdir() with self.assertRaisesRegex(preflight.IsolationError, "missing bind source"): self.validate(plan, root) def test_config_manifest_must_exist_under_exact_bind_source(self) -> None: temporary = self.plan_root(with_manifest=False) root = Path(temporary.name) plan = compose_plan(root) # A manifest elsewhere in the candidate tree cannot satisfy this bind's contract. manifest = root / "models" / "initial-candidates.yaml" manifest.parent.mkdir() manifest.write_text("candidates: []\n", encoding="utf-8") with self.assertRaisesRegex(preflight.IsolationError, "missing models/initial-candidates.yaml under its source"): self.validate(plan, root) def test_rendered_compose_network_mapping_is_accepted(self) -> None: temporary = self.plan_root() root = Path(temporary.name) plan = compose_plan(root) plan["services"]["api"]["networks"] = {"default": None} # type: ignore[index] self.assertEqual(self.validate(plan, root)["declared_networks"], ["default"]) def test_unsafe_compose_plans_are_rejected(self) -> None: cases = [ (lambda plan: plan["services"]["api"].update(network_mode="host"), "host network"), (lambda plan: plan["services"]["api"].update(container_name="modelforge-api-1"), "global container name"), (lambda plan: plan["services"]["api"]["ports"].__setitem__(0, {"host_ip": "0.0.0.0", "published": "19000", "target": 8000}), "non-loopback"), (lambda plan: plan["services"]["api"]["ports"].__setitem__(0, {"host_ip": "127.0.0.1", "published": "18000", "target": 8000}), "protected host port"), (lambda plan: plan["services"]["api"]["volumes"].__setitem__(1, {"type": "bind", "source": "/mnt/user/appdata/modelforge", "target": "/data", "read_only": True}), "escapes the candidate root"), (lambda plan: plan["services"]["api"]["volumes"].__setitem__(1, {"type": "bind", "source": "config", "target": "/app/config", "read_only": False}), "writable bind mount"), (lambda plan: plan["volumes"]["postgres-data"].update(external=True), "is external"), (lambda plan: plan["networks"]["default"].update(name="modelforge_default"), "non-project name"), ] for mutate, message in cases: with self.subTest(message=message): temporary = self.plan_root() root = Path(temporary.name) plan = compose_plan(root) mutate(plan) with self.assertRaisesRegex(preflight.IsolationError, message): self.validate(plan, root, ports={18000}) def test_protected_resource_reference_is_rejected(self) -> None: temporary = self.plan_root() root = Path(temporary.name) plan = compose_plan(root) plan["services"]["api"]["environment"] = {"UNSAFE": "modelforge-postgres-1"} # type: ignore[index] with self.assertRaisesRegex(preflight.IsolationError, "protected resources"): self.validate(plan, root, names={"modelforge-postgres-1"}) def test_only_isolated_tls_service_can_publish_test_port(self) -> None: temporary = self.plan_root() root = Path(temporary.name) source = root / "source" / "deploy" / "validation" source.mkdir(parents=True) (source / "Caddyfile").write_text("https://modelforge.example.com:8443 {}\n", encoding="utf-8") certificates = root / "tls" certificates.mkdir() (certificates / "fullchain.pem").write_text("certificate", encoding="utf-8") (certificates / "privkey.pem").write_text("key", encoding="utf-8") plan = compose_plan(root) plan["networks"]["ingress"] = {"name": f"{PROJECT}_ingress"} plan["services"]["tls"] = { "image": "caddy:2.10-alpine@sha256:4c6e91c6ed0e2fa03efd5b44747b625fec79bc9cd06ac5235a779726618e530d", "networks": ["ingress"], "read_only": True, "cap_drop": ["ALL"], "security_opt": ["no-new-privileges:true"], "volumes": [ {"type": "bind", "source": str(source / "Caddyfile"), "target": "/etc/caddy/Caddyfile", "read_only": True}, {"type": "bind", "source": str(certificates / "fullchain.pem"), "target": "/run/tls/fullchain.pem", "read_only": True}, {"type": "bind", "source": str(certificates / "privkey.pem"), "target": "/run/tls/privkey.pem", "read_only": True}, ], "ports": [{"host_ip": "0.0.0.0", "published": "18444", "target": 8443, "protocol": "tcp"}], } with self.assertRaisesRegex(preflight.IsolationError, "non-loopback"): self.validate(plan, root) self.assertEqual(preflight.validate_compose( plan, project=PROJECT, candidate_root=root, protected_ports={443, 8443}, protected_resource_names=set(), public_tls_port=18444, )["published_ports"], [18444]) plan["services"]["api"]["ports"][0] = {"host_ip": "0.0.0.0", "published": "18444", "target": 8000} with self.assertRaisesRegex(preflight.IsolationError, "non-loopback"): preflight.validate_compose( plan, project=PROJECT, candidate_root=root, protected_ports=set(), protected_resource_names=set(), public_tls_port=18444, ) def test_observed_resources_require_exact_labels_and_project_names(self) -> None: observed = { "containers": [{"name": f"{PROJECT}-api-1", "labels": {"com.docker.compose.project": PROJECT}}], "volumes": [{"name": f"{PROJECT}_postgres-data", "labels": {"com.docker.compose.project": PROJECT}}], "networks": [{"name": f"{PROJECT}_default", "labels": {"com.docker.compose.project": PROJECT}}], } self.assertEqual(preflight.validate_observed_resources(observed, project=PROJECT)["containers"], [f"{PROJECT}-api-1"]) observed["volumes"][0]["labels"] = {} with self.assertRaisesRegex(preflight.IsolationError, "no exact project label"): preflight.validate_observed_resources(observed, project=PROJECT) def test_docker_resource_collection_uses_exact_inspection(self) -> None: label = {"com.docker.compose.project": PROJECT} responses = { ("container", "ls", "--all", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "container-id\n", ("volume", "ls", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "volume-id\n", ("network", "ls", "--quiet", "--filter", f"label=com.docker.compose.project={PROJECT}"): "network-id\n", ("container", "inspect", "container-id"): '[{"Name":"/' + PROJECT + '-api-1","Config":{"Labels":{"com.docker.compose.project":"' + PROJECT + '"}}}]', ("volume", "inspect", "volume-id"): '[{"Name":"' + PROJECT + '_data","Labels":{"com.docker.compose.project":"' + PROJECT + '"}}]', ("network", "inspect", "network-id"): '[{"Name":"' + PROJECT + '_default","Labels":{"com.docker.compose.project":"' + PROJECT + '"}}]', } def run(command, **_kwargs): self.assertEqual(command[0], "docker") return type("Result", (), {"returncode": 0, "stdout": responses[tuple(command[1:])]})() with patch.object(preflight.subprocess, "run", run): observed = preflight.collect_observed_resources(PROJECT) self.assertEqual(observed["containers"][0]["labels"], label) self.assertEqual( preflight.validate_observed_resources(observed, project=PROJECT)["networks"], [f"{PROJECT}_default"], )