331 lines
13 KiB
Python
331 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import uuid
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
BROKER_ROOT = ROOT / "ops" / "gitea-deploy-broker"
|
|
INSTALLER_PATH = BROKER_ROOT / "prepare_managed_appdata_root.py"
|
|
|
|
|
|
def _load_module(name: str, path: Path) -> ModuleType | None:
|
|
helper_path = BROKER_ROOT / "managed_appdata_paths.py"
|
|
if not path.is_file() or not helper_path.is_file():
|
|
return None
|
|
if str(BROKER_ROOT) not in os.sys.path:
|
|
os.sys.path.insert(0, str(BROKER_ROOT))
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
if spec is None or spec.loader is None:
|
|
return None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture
|
|
def root_owned_test_tree():
|
|
if os.name != "posix" or not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
pytest.skip("requires a POSIX root-owned test tree")
|
|
parent = Path("/root")
|
|
if parent.is_symlink() or not parent.is_dir():
|
|
pytest.skip("requires the fixed root-owned /root test parent")
|
|
base = parent / f"appops-managed-root-test-{uuid.uuid4().hex}"
|
|
base.mkdir(mode=0o700)
|
|
base.chmod(0o700)
|
|
try:
|
|
yield base
|
|
finally:
|
|
if base.parent == parent and base.name.startswith("appops-managed-root-test-"):
|
|
shutil.rmtree(base)
|
|
|
|
|
|
def _root_safe_path(base: Path, *components: str) -> Path:
|
|
current = base
|
|
for component in components:
|
|
current = current / component
|
|
current.mkdir(mode=0o700)
|
|
current.chmod(0o700)
|
|
return current
|
|
|
|
|
|
def test_open_fixed_directory_walks_real_owned_components_without_symlinks(
|
|
root_owned_test_tree, monkeypatch
|
|
):
|
|
paths = _load_module("managed_appdata_open_real_chain_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
target = _root_safe_path(root_owned_test_tree, "docker", "volumes", "itworx-managed-appdata", "_data")
|
|
monkeypatch.setattr(paths, "MANAGED_APPDATA_ROOT", target)
|
|
|
|
descriptor = paths._open_fixed_directory(target)
|
|
try:
|
|
info = os.fstat(descriptor)
|
|
assert (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) == (0, 0, 0o700)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def test_open_fixed_directory_rejects_symlink_component(root_owned_test_tree, monkeypatch):
|
|
paths = _load_module("managed_appdata_open_symlink_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
real_target = _root_safe_path(root_owned_test_tree, "real", "managed")
|
|
alias = root_owned_test_tree / "alias"
|
|
alias.symlink_to(real_target.parent, target_is_directory=True)
|
|
target = alias / real_target.name
|
|
monkeypatch.setattr(paths, "MANAGED_APPDATA_ROOT", target)
|
|
|
|
with pytest.raises(OSError):
|
|
paths._open_fixed_directory(target)
|
|
|
|
|
|
def test_open_fixed_directory_rejects_world_writable_ancestor(root_owned_test_tree, monkeypatch):
|
|
paths = _load_module("managed_appdata_open_world_writable_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
unsafe = root_owned_test_tree / "unsafe"
|
|
unsafe.mkdir(mode=0o777)
|
|
unsafe.chmod(0o777)
|
|
target = _root_safe_path(unsafe, "managed")
|
|
monkeypatch.setattr(paths, "MANAGED_APPDATA_ROOT", target)
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot):
|
|
paths._open_fixed_directory(target)
|
|
|
|
|
|
@pytest.mark.parametrize("owner_field", ["uid", "gid"])
|
|
def test_open_fixed_directory_rejects_wrong_owner_or_group(
|
|
root_owned_test_tree, monkeypatch, owner_field
|
|
):
|
|
paths = _load_module(f"managed_appdata_open_wrong_{owner_field}_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
owner = root_owned_test_tree / "wrong-owner"
|
|
target = _root_safe_path(root_owned_test_tree, "wrong-owner", "managed")
|
|
if not hasattr(os, "chown"):
|
|
pytest.skip("requires POSIX chown for the owned test directory")
|
|
if owner_field == "uid":
|
|
os.chown(owner, 65534, 0)
|
|
else:
|
|
os.chown(owner, 0, 65534)
|
|
monkeypatch.setattr(paths, "MANAGED_APPDATA_ROOT", target)
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot):
|
|
paths._open_fixed_directory(target)
|
|
|
|
|
|
@pytest.mark.parametrize("acl_name", ["system.posix_acl_access", "system.posix_acl_default"])
|
|
def test_open_fixed_directory_rejects_reported_acl(root_owned_test_tree, monkeypatch, acl_name):
|
|
paths = _load_module("managed_appdata_open_acl_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
acl_component = root_owned_test_tree / "acl"
|
|
target = _root_safe_path(root_owned_test_tree, "acl", "managed")
|
|
acl_stat = acl_component.stat()
|
|
original_listxattr = os.listxattr
|
|
|
|
def listxattr(path, *args, **kwargs):
|
|
if isinstance(path, int):
|
|
info = os.fstat(path)
|
|
if (info.st_dev, info.st_ino) == (acl_stat.st_dev, acl_stat.st_ino):
|
|
return [acl_name]
|
|
return original_listxattr(path, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(paths, "MANAGED_APPDATA_ROOT", target)
|
|
monkeypatch.setattr(paths.os, "listxattr", listxattr)
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot, match="unreviewed ACL"):
|
|
paths._open_fixed_directory(target)
|
|
|
|
|
|
def test_fixed_controller_installer_creates_private_root_and_refuses_drift(tmp_path, monkeypatch):
|
|
installer = _load_module("managed_appdata_installer_test", INSTALLER_PATH)
|
|
assert installer is not None, "the fixed managed-appdata installer must ship with the controller artifact"
|
|
if os.name != "posix" or not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
pytest.skip("requires POSIX root ownership semantics")
|
|
|
|
target = tmp_path / "volume-root"
|
|
target.mkdir(mode=0o755)
|
|
target.chmod(0o755)
|
|
monkeypatch.setattr(installer, "MANAGED_APPDATA_ROOT", target)
|
|
monkeypatch.setattr(installer, "validate_initialization_prerequisites", lambda: True)
|
|
monkeypatch.setattr(installer, "configured_appdata_root", lambda: target)
|
|
monkeypatch.setattr(installer, "_open_fixed_directory", lambda _path: os.open(target, os.O_RDONLY | os.O_DIRECTORY))
|
|
|
|
assert installer.ensure_managed_appdata_root() == "initialized"
|
|
info = target.lstat()
|
|
assert stat.S_ISDIR(info.st_mode)
|
|
assert (info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) == (0, 0, 0o700)
|
|
marker = target / installer.MANAGED_ROOT_MARKER
|
|
assert marker.read_bytes() == installer.MANAGED_ROOT_MARKER_BYTES
|
|
assert stat.S_IMODE(marker.lstat().st_mode) == 0o400
|
|
assert installer.ensure_managed_appdata_root() == "already_initialized"
|
|
|
|
target.chmod(0o777)
|
|
with pytest.raises(installer.UnsafeManagedAppdataRoot):
|
|
installer.ensure_managed_appdata_root()
|
|
assert stat.S_IMODE(target.lstat().st_mode) == 0o777
|
|
|
|
|
|
def test_fixed_controller_installer_refuses_existing_data_without_deleting_it(tmp_path, monkeypatch):
|
|
installer = _load_module("managed_appdata_installer_reject_test", INSTALLER_PATH)
|
|
assert installer is not None
|
|
if os.name != "posix" or not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
pytest.skip("requires POSIX root ownership semantics")
|
|
|
|
target = tmp_path / "volume-root"
|
|
target.mkdir(mode=0o755)
|
|
target.chmod(0o755)
|
|
monkeypatch.setattr(installer, "MANAGED_APPDATA_ROOT", target)
|
|
monkeypatch.setattr(installer, "validate_initialization_prerequisites", lambda: True)
|
|
monkeypatch.setattr(installer, "configured_appdata_root", lambda: target)
|
|
monkeypatch.setattr(installer, "_open_fixed_directory", lambda _path: os.open(target, os.O_RDONLY | os.O_DIRECTORY))
|
|
|
|
(target / "existing-data").write_text("keep", encoding="utf-8")
|
|
with pytest.raises(installer.UnsafeManagedAppdataRoot):
|
|
installer.ensure_managed_appdata_root()
|
|
assert (target / "existing-data").read_text(encoding="utf-8") == "keep"
|
|
assert not (target / installer.MANAGED_ROOT_MARKER).exists()
|
|
|
|
|
|
def test_initialized_volume_is_idempotent_without_enumerating_application_data(tmp_path, monkeypatch):
|
|
installer = _load_module("managed_appdata_installer_idempotent_test", INSTALLER_PATH)
|
|
assert installer is not None
|
|
if os.name != "posix" or not hasattr(os, "geteuid") or os.geteuid() != 0:
|
|
pytest.skip("requires POSIX root ownership semantics")
|
|
|
|
target = tmp_path / "volume-root"
|
|
target.mkdir(mode=0o700)
|
|
(target / installer.MANAGED_ROOT_MARKER).write_bytes(installer.MANAGED_ROOT_MARKER_BYTES)
|
|
(target / installer.MANAGED_ROOT_MARKER).chmod(0o400)
|
|
(target / "app-data").mkdir()
|
|
monkeypatch.setattr(installer, "MANAGED_APPDATA_ROOT", target)
|
|
monkeypatch.setattr(installer, "validate_initialization_prerequisites", lambda: True)
|
|
monkeypatch.setattr(installer, "configured_appdata_root", lambda: target)
|
|
monkeypatch.setattr(installer, "_open_fixed_directory", lambda _path: os.open(target, os.O_RDONLY | os.O_DIRECTORY))
|
|
monkeypatch.setattr(installer.os, "listdir", lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("data must not be enumerated")))
|
|
|
|
assert installer.ensure_managed_appdata_root() == "already_initialized"
|
|
|
|
|
|
def _valid_mounts(paths):
|
|
return [
|
|
{
|
|
"Type": "volume",
|
|
"Name": paths.VOLUME_NAME,
|
|
"Source": paths.VOLUME_HOST_ROOT.as_posix(),
|
|
"Destination": paths.MANAGED_APPDATA_ROOT.as_posix(),
|
|
"Driver": "local",
|
|
"RW": True,
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Name": "",
|
|
"Source": paths.DOCKER_ROOT.as_posix(),
|
|
"Destination": paths.DOCKER_ROOT.as_posix(),
|
|
"Driver": "",
|
|
"RW": False,
|
|
},
|
|
]
|
|
|
|
|
|
def test_controller_mount_inventory_accepts_only_exact_volume_and_readonly_docker_root():
|
|
paths = _load_module("managed_appdata_mounts_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
mounts = _valid_mounts(paths)
|
|
|
|
paths.validate_controller_mounts(
|
|
mounts,
|
|
controller_id_before="a" * 64,
|
|
controller_id_after="a" * 64,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"mutate",
|
|
[
|
|
lambda m: m[0].update(Name="other-volume"),
|
|
lambda m: m[0].update(Type="bind"),
|
|
lambda m: m[0].update(Driver="nfs"),
|
|
lambda m: m[0].update(Source="/var/lib/docker/volumes/other/_data"),
|
|
lambda m: m[0].update(Destination="/managed-appdata"),
|
|
lambda m: m[0].update(RW=False),
|
|
lambda m: m[1].update(RW=True),
|
|
lambda m: m.append(dict(m[0])),
|
|
lambda m: m.append({"Type": "bind", "Source": "/var/lib/docker/volumes", "Destination": "/var/lib/docker/volumes", "RW": True}),
|
|
lambda m: m.append({"Type": "bind", "Source": "/var/lib/docker/volumes/itworx-managed-appdata/_data/app", "Destination": "/var/lib/docker/volumes/itworx-managed-appdata/_data/app", "RW": True}),
|
|
lambda m: m.append({"Type": "bind", "Source": "/var/lib/docker", "Destination": "/var/lib/docker", "RW": False}),
|
|
],
|
|
)
|
|
def test_controller_mount_inventory_fails_closed_on_mount_drift(mutate):
|
|
paths = _load_module("managed_appdata_mounts_reject_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
mounts = _valid_mounts(paths)
|
|
mutate(mounts)
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot):
|
|
paths.validate_controller_mounts(
|
|
mounts,
|
|
controller_id_before="b" * 64,
|
|
controller_id_after="b" * 64,
|
|
)
|
|
|
|
|
|
def test_controller_mount_inventory_rejects_container_replacement_during_inspection():
|
|
paths = _load_module("managed_appdata_mounts_race_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot):
|
|
paths.validate_controller_mounts(
|
|
_valid_mounts(paths),
|
|
controller_id_before="c" * 64,
|
|
controller_id_after="d" * 64,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"override",
|
|
[
|
|
{"Name": "other"},
|
|
{"Driver": "nfs"},
|
|
{"Scope": "global"},
|
|
{"Mountpoint": "/var/lib/docker/volumes/other/_data"},
|
|
{"Options": {"type": "nfs"}},
|
|
{"Labels": {"itworx.managed-by": "other"}},
|
|
],
|
|
)
|
|
def test_volume_metadata_rejects_drift(override):
|
|
paths = _load_module("managed_appdata_volume_reject_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None
|
|
document = {
|
|
"Name": paths.VOLUME_NAME,
|
|
"Driver": "local",
|
|
"Scope": "local",
|
|
"Mountpoint": paths.VOLUME_HOST_ROOT.as_posix(),
|
|
"Options": None,
|
|
"Labels": dict(paths.VOLUME_LABELS),
|
|
}
|
|
document.update(override)
|
|
|
|
with pytest.raises(paths.UnsafeManagedAppdataRoot):
|
|
paths.validate_volume_metadata(document, paths.DOCKER_ROOT.as_posix())
|
|
|
|
|
|
def test_controller_rejects_world_writable_legacy_root_without_mutating_it(tmp_path):
|
|
paths = _load_module("managed_appdata_paths_test", BROKER_ROOT / "managed_appdata_paths.py")
|
|
assert paths is not None, "the controller must centralize fixed managed-appdata path checks"
|
|
if os.name != "posix":
|
|
pytest.skip("requires POSIX permission bits")
|
|
|
|
root = tmp_path / "legacy"
|
|
root.mkdir()
|
|
paths.LEGACY_APPDATA_ROOT = root
|
|
root.chmod(0o777)
|
|
assert paths.appdata_root_is_safe(root) is False
|
|
assert stat.S_IMODE(root.lstat().st_mode) == 0o777
|
|
|
|
root.chmod(0o750)
|
|
assert paths.appdata_root_is_safe(root) is True
|