diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md index 657e609..207564d 100644 --- a/PUBLIC_SOURCE_EXPORT.md +++ b/PUBLIC_SOURCE_EXPORT.md @@ -1,3 +1,3 @@ # Public source export -This source snapshot was generated from the private canonical repository at revision `3c5fb0c6d1f4e693e4a0fa50e7151509b4450786`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. +This source snapshot was generated from the private canonical repository at revision `f8f5bf0a29159bd69accd7e58f6427ec32140640`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index b8fd65a..849945d 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "sourceRevision": "3c5fb0c6d1f4e693e4a0fa50e7151509b4450786", + "sourceRevision": "f8f5bf0a29159bd69accd7e58f6427ec32140640", "files": [ { "path": ".dockerignore", @@ -834,13 +834,13 @@ }, { "path": "backend/tests/test_controller_hub_release_volume.py", - "sha256": "2bc2345aa0ea65aa78471aac85909c671040041c44cdb67d01c127c1be17eb70", - "bytes": 18590 + "sha256": "0dddc78484ebc4a09bc75e3e71cdca5f48cb3b6afba91c3b36aa16703266d6e0", + "bytes": 21814 }, { "path": "backend/tests/test_controller_root_procedure_contract.py", - "sha256": "618a3c641191d5b9ebd904856b56d71ad1661e251c29c86f9b635ad856557ee7", - "bytes": 1866 + "sha256": "1552a883e13a66e0f154481c6f3cf2b162ad867d6bbaaed7a84cedd45c5935ac", + "bytes": 2793 }, { "path": "backend/tests/test_controller_state_reconciliation.py", diff --git a/backend/tests/test_controller_hub_release_volume.py b/backend/tests/test_controller_hub_release_volume.py index ae6d1ca..87c0608 100644 --- a/backend/tests/test_controller_hub_release_volume.py +++ b/backend/tests/test_controller_hub_release_volume.py @@ -3,6 +3,8 @@ from __future__ import annotations import importlib.util import json import os +import sys +import types from pathlib import Path import pytest @@ -415,6 +417,85 @@ def test_source_compose_preflight_rejects_host_file_reads_before_compose(compose assert calls == [] +@pytest.mark.parametrize( + "source", + [ + b"services:\n app:\n 'env_file': /tmp/secret\n", + b"services: {app: {\"include\": ./untrusted.yml}}\n", + b"services:\n - \"build\": ./untrusted\n", + b"services:\n app:\n \"\\u0065nv_file\": /tmp/secret\n", + ], +) +def test_source_compose_preflight_rejects_quoted_or_escaped_file_keys(source): + with pytest.raises(module.VolumeTransitionError): + module._preflight_source_compose(source) + + +def test_preparer_uses_the_pinned_bundled_yaml_parser(): + assert module.yaml.__version__ == "6.0.2" + assert module.yaml.__name__.startswith("_appops_controller_yaml_") + assert module.yaml.__with_libyaml__ is False + assert Path(module.yaml.__file__).resolve().parent == ROOT / "scripts/vendor/yaml" + + +def test_preparer_does_not_replace_a_host_cached_yaml_module(monkeypatch): + host_yaml = types.ModuleType("yaml") + host_yaml.__file__ = "/host/site-packages/yaml/__init__.py" + monkeypatch.setitem(sys.modules, "yaml", host_yaml) + original_sys_path = sys.path.copy() + module_name = "_controller_volume_import_isolation_test" + spec = importlib.util.spec_from_file_location( + module_name, ROOT / "scripts/prepare_controller_hub_release_volume.py" + ) + assert spec is not None and spec.loader is not None + isolated_module = importlib.util.module_from_spec(spec) + monkeypatch.setitem(sys.modules, module_name, isolated_module) + spec.loader.exec_module(isolated_module) + assert sys.modules["yaml"] is host_yaml + assert isolated_module.yaml.__name__.startswith("_appops_controller_yaml_") + assert isolated_module.yaml.safe_load(b"services: {}") == {"services": {}} + assert sys.path == original_sys_path + + +def test_source_compose_preflight_ignores_comments_strings_and_block_scalars(): + source = ( + b"name: gitea-deploy-runners\n" + b"# include: ./disabled.yml\n" + b"services:\n app:\n" + b" command: |\n" + b" echo 'env_file: is command text'\n" + b" echo include: is command text\n" + b" labels: {note: \"build: is a label value\"}\n" + ) + module._preflight_source_compose(source) + + +def test_source_compose_preflight_rejects_unresolvable_alias_keys(): + source = b"services:\n app:\n *unsafe-key: value\n" + with pytest.raises(module.VolumeTransitionError, match="compose_source_invalid"): + module._preflight_source_compose(source) + + +@pytest.mark.parametrize( + "source", + [ + b"services: {}\nconfigs:\n tls-material:\n file: /etc/ssl/private/key.pem\n", + b"services: {}\nsecrets: {gitea-token: {environment: GITEA_TOKEN}}\n", + ], +) +def test_source_compose_preflight_rejects_local_config_and_secret_sources(source): + with pytest.raises(module.VolumeTransitionError, match="external_source_invalid"): + module._preflight_source_compose(source) + + +def test_source_compose_preflight_allows_external_config_and_service_environment_fields(): + source = ( + b"configs:\n tls-material:\n external: true\n" + b"services:\n app:\n environment:\n file: no-local-file-read\n" + ) + module._preflight_source_compose(source) + + def test_capture_program_is_fixed_descriptor_based_and_compiles(): legacy = module._controller_capture_source(config_root_in_code=True) protected = module._controller_capture_source(config_root_in_code=False) diff --git a/backend/tests/test_controller_root_procedure_contract.py b/backend/tests/test_controller_root_procedure_contract.py index 2116737..31f1e53 100644 --- a/backend/tests/test_controller_root_procedure_contract.py +++ b/backend/tests/test_controller_root_procedure_contract.py @@ -4,7 +4,6 @@ import re import shlex from pathlib import Path - ROOT = Path(__file__).resolve().parents[2] @@ -16,13 +15,30 @@ def test_windows_wrapper_archives_every_file_required_by_root_installer(): archived = re.search(r"& git archive --format=tar .*? \$SourceRevision (.*?)\r?\n", wrapper) assert required is not None and archived is not None archive_paths = set(shlex.split(archived.group(1))) + required_paths = shlex.split(required.group(1)) assert all( path in archive_paths or any(path.startswith(f"{directory}/") for directory in archive_paths) - for path in shlex.split(required.group(1)) + for path in required_paths ) + vendor_sources = sorted( + path.relative_to(ROOT).as_posix() + for path in (ROOT / "scripts/vendor").rglob("*") + if path.is_file() and (path.suffix == ".py" or path.name == "pyyaml-LICENSE") + ) + required_vendor_sources = sorted( + path for path in required_paths if path.startswith("scripts/vendor/") + ) + assert vendor_sources == required_vendor_sources assert "scripts/prepare_controller_hub_release_volume.py" in archived.group(1) assert "scripts/upgrade_deployment_controller.sh" in archived.group(1) + assert "scripts/vendor" in archived.group(1) + assert "scripts/vendor/pyyaml-LICENSE" in required.group(1) + assert "scripts/vendor/yaml/__init__.py" in required.group(1) + assert 'mkdir -m 0700 "${work}/vendor" "${work}/vendor/yaml"' in installer + assert '"${work}/vendor/yaml/${vendor_file}"' in installer + assert ('[[ -d "${script_dir}/vendor" && ! -L "${script_dir}/vendor" && ' + '-d "${script_dir}/vendor/yaml" && ! -L "${script_dir}/vendor/yaml" ]]') in installer def test_windows_wrapper_supplies_private_archive_and_exact_image_identity():