340 lines
12 KiB
Python
340 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
MODULE_PATH = ROOT / "ops/gitea-deploy-broker/forge_backup_paths.py"
|
|
if MODULE_PATH.is_file():
|
|
spec = importlib.util.spec_from_file_location("forge_backup_paths_test", MODULE_PATH)
|
|
assert spec is not None and spec.loader is not None
|
|
paths = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(paths)
|
|
else:
|
|
paths = None
|
|
|
|
|
|
def _paths():
|
|
if paths is None:
|
|
pytest.fail("fixed Forge backup path verifier has not been implemented", pytrace=False)
|
|
return paths
|
|
|
|
|
|
def volume_metadata():
|
|
return {
|
|
"Name": "itworx-forge-master-key-backup",
|
|
"Driver": "local",
|
|
"Scope": "local",
|
|
"Options": None,
|
|
"Labels": {
|
|
"itworx.managed-by": "deployment-controller",
|
|
"itworx.purpose": "forge-master-key-backup",
|
|
},
|
|
"Mountpoint": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
|
}
|
|
|
|
|
|
def controller_mounts():
|
|
return [
|
|
{
|
|
"Type": "volume",
|
|
"Name": "itworx-forge-master-key-backup",
|
|
"Driver": "local",
|
|
"Source": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
|
"Destination": "/forge-master-key-backup",
|
|
"RW": True,
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Name": "",
|
|
"Source": "/var/lib/docker",
|
|
"Destination": "/var/lib/docker",
|
|
"RW": False,
|
|
},
|
|
{
|
|
"Type": "volume",
|
|
"Name": "itworx-managed-appdata",
|
|
"Driver": "local",
|
|
"Source": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"Destination": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
|
"RW": True,
|
|
},
|
|
{
|
|
"Type": "volume",
|
|
"Name": "itworx-controller-hub-releases",
|
|
"Driver": "local",
|
|
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data",
|
|
"Destination": "/controller-hub-releases",
|
|
"RW": True,
|
|
},
|
|
{
|
|
"Type": "bind",
|
|
"Name": "",
|
|
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data/controller-config",
|
|
"Destination": "/controller-config",
|
|
"RW": False,
|
|
},
|
|
]
|
|
|
|
|
|
def mountinfo(*, backup_options="rw,nosuid,nodev", docker_options="ro,relatime"):
|
|
base = [
|
|
"35 25 8:1 / / rw,relatime - ext4 /dev/sda1 rw,relatime",
|
|
f"41 35 8:2 / /var/lib/docker {docker_options} - ext4 /dev/sda2 ro,relatime",
|
|
f"42 35 8:3 / /forge-master-key-backup {backup_options} - ext4 /dev/sda3 rw,relatime",
|
|
]
|
|
base.extend(
|
|
f"{50 + index} 41 8:{index + 4} / /var/lib/docker/containers/c{index}/mount{index} "
|
|
f"{'ro' if index % 2 else 'rw'},relatime - ext4 /dev/sdb{index} rw,relatime"
|
|
for index in range(35)
|
|
)
|
|
return "\n".join(base)
|
|
|
|
|
|
def test_exact_local_backup_volume_metadata_is_accepted():
|
|
module = _paths()
|
|
assert module.validate_volume_metadata(volume_metadata(), "/var/lib/docker") is None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value"),
|
|
[
|
|
("Name", "foreign"),
|
|
("Driver", "nfs"),
|
|
("Scope", "global"),
|
|
("Options", {"device": "/mnt/share"}),
|
|
("Options", []),
|
|
("Labels", {"itworx.managed-by": "deployment-controller"}),
|
|
("Labels", {
|
|
"itworx.managed-by": "deployment-controller",
|
|
"itworx.purpose": "managed-appdata",
|
|
}),
|
|
("Mountpoint", "/mnt/foreign"),
|
|
],
|
|
)
|
|
def test_backup_volume_metadata_drift_is_rejected(field, value):
|
|
module = _paths()
|
|
item = volume_metadata()
|
|
item[field] = value
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module.validate_volume_metadata(item, "/var/lib/docker")
|
|
|
|
|
|
def test_backup_volume_metadata_requires_the_fixed_docker_root():
|
|
module = _paths()
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module.validate_volume_metadata(volume_metadata(), "/mnt/docker")
|
|
|
|
|
|
def test_mount_contract_allows_only_the_fixed_rw_backup_and_ro_docker_root():
|
|
module = _paths()
|
|
assert module.validate_controller_mounts(
|
|
controller_mounts(),
|
|
controller_id_before="a" * 64,
|
|
controller_id_after="a" * 64,
|
|
) is None
|
|
|
|
|
|
def test_mount_contract_allows_managed_appdata_to_be_absent():
|
|
module = _paths()
|
|
mounts = controller_mounts()
|
|
mounts.pop(2)
|
|
assert module.validate_controller_mounts(
|
|
mounts,
|
|
controller_id_before="a" * 64,
|
|
controller_id_after="a" * 64,
|
|
) is None
|
|
|
|
|
|
def test_mount_contract_allows_only_the_exact_managed_appdata_nested_volume():
|
|
module = _paths()
|
|
mounts = controller_mounts()
|
|
module.validate_controller_mounts(
|
|
mounts,
|
|
controller_id_before="a" * 64,
|
|
controller_id_after="a" * 64,
|
|
)
|
|
mounts[-3]["Destination"] = "/var/lib/docker/volumes/foreign/_data"
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module.validate_controller_mounts(
|
|
mounts,
|
|
controller_id_before="a" * 64,
|
|
controller_id_after="a" * 64,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"damage",
|
|
[
|
|
"controller_changed",
|
|
"backup_rw_disabled",
|
|
"backup_name_changed",
|
|
"backup_source_changed",
|
|
"backup_destination_changed",
|
|
"backup_driver_changed",
|
|
"duplicate_backup_mount",
|
|
"backup_source_alias",
|
|
"backup_target_child_alias",
|
|
"backup_target_parent_alias",
|
|
"docker_root_rw",
|
|
"docker_root_child_mount",
|
|
"managed_appdata_duplicate",
|
|
"managed_appdata_source_changed",
|
|
"managed_appdata_driver_changed",
|
|
"managed_appdata_target_alias",
|
|
"managed_appdata_name_alias",
|
|
"root_exposure",
|
|
],
|
|
)
|
|
def test_mount_contract_rejects_aliases_and_mount_drift(damage):
|
|
module = _paths()
|
|
mounts = controller_mounts()
|
|
before = after = "a" * 64
|
|
if damage == "controller_changed":
|
|
after = "b" * 64
|
|
elif damage == "backup_rw_disabled":
|
|
mounts[0]["RW"] = False
|
|
elif damage == "backup_name_changed":
|
|
mounts[0]["Name"] = "foreign"
|
|
elif damage == "backup_source_changed":
|
|
mounts[0]["Source"] = "/mnt/foreign"
|
|
elif damage == "backup_destination_changed":
|
|
mounts[0]["Destination"] = "/backup-copy"
|
|
elif damage == "backup_driver_changed":
|
|
mounts[0]["Driver"] = "nfs"
|
|
elif damage == "duplicate_backup_mount":
|
|
mounts.append(dict(mounts[0]))
|
|
elif damage == "backup_source_alias":
|
|
mounts.append({
|
|
"Type": "bind", "Source": volume_metadata()["Mountpoint"],
|
|
"Destination": "/backup-copy", "RW": False,
|
|
})
|
|
elif damage == "backup_target_child_alias":
|
|
mounts.append({
|
|
"Type": "bind", "Source": "/foreign", "Destination": "/forge-master-key-backup/state",
|
|
"RW": True,
|
|
})
|
|
elif damage == "backup_target_parent_alias":
|
|
mounts.append({
|
|
"Type": "bind", "Source": "/foreign", "Destination": "/",
|
|
"RW": True,
|
|
})
|
|
elif damage == "docker_root_rw":
|
|
mounts[1]["RW"] = True
|
|
elif damage == "docker_root_child_mount":
|
|
mounts.append({
|
|
"Type": "bind", "Source": "/var/lib/docker/volumes/other",
|
|
"Destination": "/var/lib/docker/volumes/other", "RW": True,
|
|
})
|
|
elif damage == "managed_appdata_duplicate":
|
|
mounts.append(dict(mounts[2]))
|
|
elif damage == "managed_appdata_source_changed":
|
|
mounts[2]["Source"] = "/var/lib/docker/volumes/foreign/_data"
|
|
elif damage == "managed_appdata_driver_changed":
|
|
mounts[2]["Driver"] = "nfs"
|
|
elif damage == "managed_appdata_target_alias":
|
|
mounts[2]["Destination"] = "/appdata-copy"
|
|
elif damage == "managed_appdata_name_alias":
|
|
mounts[2].update({
|
|
"Source": "/var/lib/docker/volumes/foreign/_data",
|
|
"Destination": "/appdata-copy",
|
|
})
|
|
else:
|
|
mounts.append({
|
|
"Type": "bind", "Source": "/", "Destination": "/", "RW": True,
|
|
})
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module.validate_controller_mounts(
|
|
mounts, controller_id_before=before, controller_id_after=after
|
|
)
|
|
|
|
|
|
def test_proc_mountinfo_requires_the_exact_rw_backup_and_ro_docker_mounts():
|
|
module = _paths()
|
|
assert module._validate_mountinfo_contract(mountinfo()) is None
|
|
for unsafe in (
|
|
mountinfo(backup_options="ro,nosuid,nodev"),
|
|
mountinfo(docker_options="rw,relatime"),
|
|
mountinfo().replace("/forge-master-key-backup", "/backup-copy"),
|
|
mountinfo() + "\n43 35 8:4 / /forge-master-key-backup rw - ext4 /dev/sda4 rw",
|
|
):
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module._validate_mountinfo_contract(unsafe)
|
|
|
|
|
|
@pytest.mark.skipif(os.name == "nt", reason="directory inode checks require POSIX descriptors")
|
|
def test_directory_identity_binds_backup_mount_to_the_fixed_docker_volume_source(tmp_path):
|
|
module = _paths()
|
|
source = tmp_path / "source"
|
|
mounted = tmp_path / "mounted"
|
|
source.mkdir()
|
|
mounted.mkdir()
|
|
source_fd = os.open(source, os.O_RDONLY)
|
|
mounted_fd = os.open(mounted, os.O_RDONLY)
|
|
same_fd = os.open(source, os.O_RDONLY)
|
|
try:
|
|
module._require_same_directory(source_fd, same_fd)
|
|
with pytest.raises(module.UnsafeForgeBackupVolume, match="mount_source_mismatch"):
|
|
module._require_same_directory(source_fd, mounted_fd)
|
|
finally:
|
|
os.close(source_fd)
|
|
os.close(mounted_fd)
|
|
os.close(same_fd)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
reason="directory ownership checks require root on POSIX",
|
|
)
|
|
def test_fixed_directory_walk_rejects_symlinks_and_unsafe_modes(tmp_path):
|
|
module = _paths()
|
|
volume_root = tmp_path / "volume"
|
|
volume_root.mkdir(mode=0o755)
|
|
volume_root.chmod(0o755)
|
|
fd = module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
|
os.close(fd)
|
|
|
|
linked = tmp_path / "linked"
|
|
linked.symlink_to(volume_root, target_is_directory=True)
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module._open_directory_no_symlinks(linked, anchor=tmp_path)
|
|
|
|
volume_root.chmod(0o777)
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
|
reason="directory ACL checks require root on POSIX",
|
|
)
|
|
def test_fixed_directory_walk_rejects_access_acl(tmp_path, monkeypatch):
|
|
module = _paths()
|
|
volume_root = tmp_path / "volume"
|
|
volume_root.mkdir(mode=0o755)
|
|
volume_root.chmod(0o755)
|
|
monkeypatch.setattr(module.os, "listxattr", lambda _fd: ["system.posix_acl_access"])
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
|
|
|
|
|
def test_read_only_identity_check_fails_closed_without_creating_or_repairing_paths(tmp_path, monkeypatch):
|
|
module = _paths()
|
|
sentinel = tmp_path / "unchanged"
|
|
sentinel.write_bytes(b"keep")
|
|
bad = volume_metadata()
|
|
bad["Labels"]["itworx.purpose"] = "foreign-purpose"
|
|
monkeypatch.setattr(module, "_read_volume_snapshot", lambda: (bad, "/var/lib/docker"))
|
|
monkeypatch.setattr(
|
|
module,
|
|
"_open_fixed_backup_root",
|
|
lambda: pytest.fail("unsafe volume metadata must fail before opening the data root"),
|
|
)
|
|
with pytest.raises(module.UnsafeForgeBackupVolume):
|
|
module.validate_forge_backup_volume()
|
|
assert sentinel.read_bytes() == b"keep"
|
|
assert sorted(path.name for path in tmp_path.iterdir()) == ["unchanged"]
|