Publish curated Unraid-AppOps-Gateway source from ad0a390e431166c3198a2ac3bec010c2b44f2dea

This commit is contained in:
Public Source Publisher committed 2026-10-02 09:56:13 +00:00
1 parent 29c1e3d5a1
commit 98d31477f9
35 files changed
+7264 -185

No files matched your search

@@ -38,6 +38,7 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
"/mnt/cache:/mnt/cache",
"/mnt/user:/mnt/user",
"/srv/example/appdata/gitea/git/repositories:/gitea-repositories:ro",
"/var/lib/docker:/var/lib/docker:ro",
"/var/run/docker.sock:/var/run/docker.sock",
}
assert spec["Cmd"][:2] == ["/bin/bash", "-lc"]
@@ -47,6 +48,13 @@ def test_controller_promoter_helper_spec_is_fixed_and_bounded():
assert revision in command
assert CONTROLLER_VERSION in command
assert "/gitea-repositories/jens/unraid-appops-gateway.git" in command
assert f"itworx-controller-artifact-promoter-{revision[:12]}.XXXXXX" in command
assert "work='/mnt/cache" not in command
assert "rm -rf -- \"$work\"" in command
assert "chmod 0600 \"$work/controller.tar\"" in command
assert "scripts/prepare_controller_hub_release_volume.py" in command
assert "scripts/upgrade_deployment_controller.sh > \"$work/controller.tar\"" in command
assert f"--image-id '{image_id}'" in command
@pytest.mark.parametrize(
@@ -0,0 +1,116 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def load_paths():
spec = importlib.util.spec_from_file_location(
"controller_config_paths_test", ROOT / "ops/gitea-deploy-broker/controller_paths.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_production_code_uses_fixed_protected_mutable_config():
paths = load_paths()
code = Path("/opt/gitea-deploy")
assert paths.controller_config_root(code) == Path("/controller-hub-releases/controller-config")
assert paths.controller_readonly_config_root(code) == Path("/controller-config")
assert paths.controller_config_root(code) != code
def test_private_install_stage_keeps_its_captured_config(tmp_path):
paths = load_paths()
stage = tmp_path / "controller"
assert paths.controller_config_root(stage) == stage
assert paths.controller_readonly_config_root(stage) == stage
def test_config_location_has_no_environment_override(monkeypatch):
paths = load_paths()
monkeypatch.setenv("ITWORX_CONTROLLER_CONFIG_ROOT", "/untrusted")
monkeypatch.setenv("CONTROLLER_CONFIG_ROOT", "/untrusted")
assert paths.controller_config_root(Path("/opt/gitea-deploy")) == Path(
"/controller-hub-releases/controller-config"
)
def test_repopulse_store_keeps_readonly_and_mutable_config_aliases():
paths = load_paths()
monkeypatch_path = ROOT / "ops/gitea-deploy-broker"
import sys
if str(monkeypatch_path) not in sys.path:
sys.path.insert(0, str(monkeypatch_path))
spec = importlib.util.spec_from_file_location(
"repopulse_config_alias_test", monkeypatch_path / "repopulse_adoption_store.py"
)
store = importlib.util.module_from_spec(spec)
spec.loader.exec_module(store)
code = Path("/opt/gitea-deploy")
assert store.CONTROLLER_READONLY_ROOT == paths.controller_readonly_config_root(code)
assert store.CONTROLLER_RW_ROOT == paths.controller_config_root(code)
assert store.CONTROLLER_READONLY_ROOT != store.CONTROLLER_RW_ROOT
def test_registry_updates_use_mutable_config_and_preserve_code(monkeypatch, tmp_path):
monkeypatch.syspath_prepend(str(ROOT / "ops/gitea-deploy-broker"))
import controller_paths
config = tmp_path / "config"
config.mkdir(mode=0o755)
registry = config / "projects.json"
registry.write_text("{}\n", encoding="utf-8")
registry.chmod(0o600)
code = ROOT / "ops/gitea-deploy-broker"
original_code = (code / "status_server.py").read_bytes()
monkeypatch.setattr(controller_paths, "controller_config_root", lambda _root: config)
spec = importlib.util.spec_from_file_location("mutable_config_controller_test", code / "status_server.py")
controller = importlib.util.module_from_spec(spec)
spec.loader.exec_module(controller)
assert controller.PROJECTS_PATH == registry
assert controller.WEBHOOK_SECRET_ROOT == config / "webhook-secrets"
assert controller.APPOPS_DEPLOYMENT_SECRET_ROOT == config / "appops-deployment-secrets"
assert controller.GITEA_VALIDATION_TOKEN_FILE == config / "gitea-validation-token"
assert controller.PRIVATE_TLS_CONFIG_PATH == config / "private-tls.json"
assert controller.CONTROLLER_RELEASE_PATH == code / "controller-release.json"
controller._write_projects({"jens/example": {"slug": "example"}})
assert controller._load_projects() == {"jens/example": {"slug": "example"}}
assert registry.stat().st_mode & 0o777 == 0o600
assert (code / "status_server.py").read_bytes() == original_code
def test_status_reads_hub_state_through_the_fixed_pinned_adapter(monkeypatch, tmp_path):
monkeypatch.syspath_prepend(str(ROOT / "ops/gitea-deploy-broker"))
import mcp_hub_release_store
code = ROOT / "ops/gitea-deploy-broker"
spec = importlib.util.spec_from_file_location("hub_state_controller_test", code / "status_server.py")
controller = importlib.util.module_from_spec(spec)
spec.loader.exec_module(controller)
state = tmp_path / "state"
state.mkdir()
(state / "itworx-mcp-hub.json").write_text('{"live_revision":"untrusted-path"}')
(state / "ordinary-project.json").write_text('{"live_revision":"ordinary"}')
monkeypatch.setattr(controller, "STATE_ROOT", state)
monkeypatch.setattr(
mcp_hub_release_store, "read_hub_state_bytes", lambda: b'{"live_revision":"pinned"}'
)
assert controller._deployment_state("itworx-mcp-hub") == {"live_revision": "pinned"}
assert controller._deployment_state("ordinary-project") == {"live_revision": "ordinary"}
def unsafe():
raise mcp_hub_release_store.ReleaseStoreError("state path changed")
monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", unsafe)
assert controller._deployment_state("itworx-mcp-hub") == {}
monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", lambda: b'[]')
assert controller._deployment_state("itworx-mcp-hub") == {}
monkeypatch.setattr(mcp_hub_release_store, "read_hub_state_bytes", lambda: b'\xff')
assert controller._deployment_state("itworx-mcp-hub") == {}
@@ -0,0 +1,461 @@
from __future__ import annotations
import importlib.util
import json
import os
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location(
"controller_hub_volume", ROOT / "scripts/prepare_controller_hub_release_volume.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
REVISION = "a" * 40
IMAGE_ID = "sha256:" + "b" * 64
ORIGINAL_COMPOSE = b"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: immutable-controller\n"
def volume():
return {
"Name": module.VOLUME,
"Driver": "local",
"Scope": "local",
"Options": None,
"Labels": dict(module.LABELS),
"Mountpoint": module.MOUNTPOINT.as_posix(),
}
@pytest.mark.parametrize(
("key", "value"),
[
("Name", "operator-volume"),
("Driver", "nfs"),
("Scope", "global"),
("Options", {"device": "/mnt/cache/appdata"}),
("Labels", {}),
("Mountpoint", "/mnt/cache/unsafe"),
],
)
def test_unknown_or_redirected_volume_is_never_adopted(key, value):
item = volume()
item[key] = value
with pytest.raises(module.VolumeTransitionError, match="identity_invalid"):
module.validate_volume(item)
def test_only_the_exact_local_volume_is_accepted():
module.validate_volume(volume())
def model():
return {
"name": "gitea-deploy-runners",
"services": {
"deploy-control": {
"container_name": module.CONTAINER,
"image": "immutable-controller",
"volumes": [
{
"type": "bind",
"source": module.DOCKER_ROOT.as_posix(),
"target": module.DOCKER_ROOT.as_posix(),
"read_only": True,
},
{
"type": "bind",
"source": module.CONTROLLER_DIR.as_posix(),
"target": module.EXECUTABLE_TARGET,
"read_only": True,
},
],
"environment": {"PRIVATE_SETTING": "unchanged"},
},
"runner": {
"image": "pinned-runner",
"volumes": [
{"type": "bind", "source": "/fixed/data", "target": "/data"}
],
},
},
}
@pytest.fixture
def compose(monkeypatch):
baseline = model()
calls = []
def run(arguments, *, data=None, **_kwargs):
calls.append((arguments, data))
if arguments[:3] == ["docker", "image", "inspect"]:
return IMAGE_ID.encode("ascii")
if arguments[-2:] == ["--format", "json"]:
return data if data and data.startswith(b"{") else json.dumps(baseline).encode()
return b""
monkeypatch.setattr(module, "_run", run)
return baseline, calls
def test_compose_uses_protected_code_and_two_config_views(compose):
baseline, calls = compose
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
service = rendered["services"]["deploy-control"]
assert service["image"] == IMAGE_ID
assert service["pull_policy"] == "never"
volumes = service["volumes"]
assert {
"type": "bind",
"source": module._expected_revision_path(REVISION).as_posix(),
"target": module.EXECUTABLE_TARGET,
"read_only": True,
} in volumes
assert {
"type": "volume",
"source": module.VOLUME,
"target": module.TARGET,
"volume": {"nocopy": True},
} in volumes
assert {
"type": "bind",
"source": module._expected_config_path().as_posix(),
"target": module.CONFIG_TARGET,
"read_only": True,
} in volumes
assert len(volumes) == 4
volumes.remove(next(item for item in volumes if item["target"] == module.EXECUTABLE_TARGET))
volumes.remove(next(item for item in volumes if item["target"] == module.TARGET))
volumes.remove(next(item for item in volumes if item["target"] == module.CONFIG_TARGET))
assert rendered["volumes"] == {
module.VOLUME: {"name": module.VOLUME, "external": True}
}
assert rendered["services"]["runner"] == baseline["services"]["runner"]
assert service["environment"] == baseline["services"]["deploy-control"]["environment"]
compose_calls = [
arguments for arguments, _ in calls if arguments[:2] == ["docker", "compose"]
]
assert compose_calls
assert all("--project-directory" in arguments for arguments in compose_calls)
assert all(module.MOUNTPOINT.as_posix() in arguments for arguments in compose_calls)
assert all("--env-file" in arguments and "/dev/null" in arguments for arguments in compose_calls)
assert all("-f" in arguments and "-" in arguments for arguments in compose_calls)
assert calls[0][1] == ORIGINAL_COMPOSE
@pytest.mark.parametrize("damage", ["target_collision", "foreign_volume", "graph_rw", "relative_bind", "env_file"])
def test_compose_rejects_unsafe_or_unpinned_activation_inputs(compose, damage):
baseline, _ = compose
if damage == "target_collision":
baseline["services"]["deploy-control"]["volumes"].append(
{"type": "bind", "source": "/unsafe", "target": module.TARGET}
)
elif damage == "foreign_volume":
baseline["services"]["runner"]["volumes"].append(
{"type": "volume", "source": module.VOLUME, "target": "/foreign"}
)
elif damage == "graph_rw":
baseline["services"]["deploy-control"]["volumes"][0]["read_only"] = False
elif damage == "relative_bind":
baseline["services"]["runner"]["volumes"][0]["source"] = "./unsafe"
else:
baseline["services"]["deploy-control"]["env_file"] = [".env"]
with pytest.raises(module.VolumeTransitionError):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
def test_compose_tag_replacement_cannot_change_controller_startup_image(compose, monkeypatch):
_baseline, calls = compose
original_run = module._run
def replaced_tag(arguments, **kwargs):
if arguments[:3] == ["docker", "image", "inspect"]:
return ("sha256:" + "c" * 64).encode("ascii")
return original_run(arguments, **kwargs)
monkeypatch.setattr(module, "_run", replaced_tag)
with pytest.raises(module.VolumeTransitionError, match="image_reference_changed"):
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
assert not any(
arguments[-1:] == ["deploy-control"] and "up" in arguments
for arguments, _ in calls
)
def test_config_snapshot_has_exact_allowlist_and_broker_token():
entries = {
"projects.json": (b"{}", 0o600),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
"private-tls.json": (b"{}", 0o600),
"appops-enrollment-bootstrap": (b"secret", 0o600),
"webhook-secrets/forge": (b"secret", 0o600),
"appops-deployment-secrets/repopulse": (b"secret", 0o600),
"status_server.py": (b"code", 0o644),
}
directories = {
"": 0o755,
"webhook-secrets": 0o700,
"appops-deployment-secrets": 0o700,
}
config, config_dirs = module._config_tree(entries, directories)
assert set(config) == set(entries) - {"status_server.py"}
assert "broker-token" in config
assert config_dirs == directories
def test_unknown_config_file_is_rejected():
entries = {
"projects.json": (b"{}", 0o600),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
"shell.py": (b"unexpected", 0o600),
}
with pytest.raises(module.VolumeTransitionError, match="config_snapshot_unknown"):
module._config_tree(entries, {"": 0o755})
def test_partial_config_tree_is_accepted_only_for_crash_resume():
partial = {"projects.json": (b"{}", 0o600), "broker-token": (b"secret", 0o600)}
assert module._config_tree(partial, {"": 0o755}, allow_partial=True) == (
partial, {"": 0o755}
)
with pytest.raises(module.VolumeTransitionError, match="snapshot_incomplete"):
module._config_tree(partial, {"": 0o755})
def test_config_snapshot_requires_private_mode_for_every_file():
entries = {
"projects.json": (b"{}", 0o644),
"broker-token": (b"secret", 0o600),
"gitea-validation.json": (b"{}", 0o600),
"gitea-validation-token": (b"secret", 0o600),
}
with pytest.raises(module.VolumeTransitionError, match="config_file_unsafe"):
module._config_tree(entries, {"": 0o755})
def test_previous_private_tls_is_validated_from_the_attested_config_snapshot(
tmp_path, monkeypatch
):
previous = tmp_path / "previous-controller"
previous.mkdir(mode=0o700)
(previous / "status_server.py").write_text("# captured validator\n", encoding="utf-8")
(previous / "private-tls.json").write_bytes(b"captured private config")
calls = []
monkeypatch.setattr(module, "_run", lambda arguments, **kwargs: calls.append((arguments, kwargs)) or b"")
module._validate_previous_private_tls(tmp_path)
assert calls == [(
["python3", str(previous / "status_server.py"), "--check-private-tls-config"],
{"max_output": 4096},
)]
(previous / "private-tls.json").unlink()
calls.clear()
module._validate_previous_private_tls(tmp_path)
assert calls == []
@pytest.mark.skipif(os.name == "nt", reason="private TLS snapshot guard requires POSIX symlink semantics")
def test_previous_private_tls_rejects_symlinked_snapshot(tmp_path):
previous = tmp_path / "previous-controller"
previous.mkdir(mode=0o700)
(previous / "status_server.py").write_text("# captured validator\n", encoding="utf-8")
target = tmp_path / "tls.json"
target.write_bytes(b"unsafe")
(previous / "private-tls.json").symlink_to(target)
with pytest.raises(module.VolumeTransitionError, match="private_tls_unsafe"):
module._validate_previous_private_tls(tmp_path)
@pytest.mark.parametrize("kind", ["artifacts", "config"])
def test_writer_is_exact_image_only_bounded_and_has_no_host_paths(kind):
command = module._writer_spec(IMAGE_ID, REVISION, kind=kind)
assert command[0:3] == ["docker", "create", "--name"]
assert module.WRITER_NAME in command
assert IMAGE_ID in command
assert "--network" in command and command[command.index("--network") + 1] == "none"
assert "--read-only" in command and "--cap-drop" in command
assert "--cpu-quota" in command and "--memory" in command and "--pids-limit" in command
assert "--mount" in command
mount = command[command.index("--mount") + 1]
assert mount == f"type=volume,source={module.VOLUME},target={module.TARGET},volume-nocopy"
assert not any(item.startswith(("/mnt/", "/var/lib/")) for item in command)
def test_promoted_compose_actions_use_only_stdin_and_fixed_service(compose):
baseline, calls = compose
rendered = module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
module.compose_action(rendered, "stop")
module.compose_action(rendered, "up")
assert calls[-2][0][-2:] == ["stop", module.SERVICE]
assert calls[-1][0][-5:] == ["up", "-d", "--force-recreate", "--pull", "never", module.SERVICE][-5:]
assert calls[-2][1] == rendered
assert calls[-1][1] == rendered
assert baseline["name"] == "gitea-deploy-runners"
def test_installer_has_no_shared_executable_stage_or_compose_path():
source = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
assert "mktemp -d -p /tmp \"itworx-controller-upgrade-" in source
assert 'docker compose --project-directory "$ROOT" -f "$COMPOSE_FILE"' not in source
assert 'python3 "${work}/${PREPARER_NAME}"' in source
assert 'mv -- "$CONTROLLER_DIR"' not in source
assert 'cp -a -- "$CONTROLLER_DIR"' not in source
assert "ops/gitea-deploy-broker/mcp_hub_release_store.py" in source
def test_installer_release_order_matches_renderer_contract():
renderer_spec = importlib.util.spec_from_file_location(
"controller_renderer", ROOT / "scripts/prepare_controller_artifact.py"
)
renderer = importlib.util.module_from_spec(renderer_spec)
renderer_spec.loader.exec_module(renderer)
assert tuple(module.RELEASE_FILES) == tuple(renderer._ARTIFACT_FILES)
assert module.RELEASE_FILES[:3] == (
"controller_paths.py", "mcp_hub_release_store.py", "deploy.py"
)
@pytest.mark.skipif(os.name == "nt", reason="dirfd swap test requires POSIX openat")
def test_mirror_updates_stay_on_the_open_directory_after_path_swap(tmp_path, monkeypatch):
original = tmp_path / "controller"
moved = tmp_path / "controller-pinned"
original.mkdir()
before_entries = {
"controller_paths.py": (b"prior paths", 0o644),
"mcp_hub_release_store.py": (b"prior Hub helper", 0o644),
}
for name, (value, _mode) in before_entries.items():
(original / name).write_bytes(value)
prior = {
name: (f"prior:{name}".encode(), 0o644)
for name in module.LEGACY_RELEASE_FILES_2_1_13
}
prior["controller-release.json"] = (b"prior manifest", 0o644)
updated = {
name: (f"updated:{name}".encode(), 0o644)
for name in module.RELEASE_FILES
}
updated["controller-release.json"] = (b"updated manifest", 0o644)
prior_tar = module._release_tar_bytes("b" * 40, (prior, {"": 0o755}))
updated_tar = module._release_tar_bytes("c" * 40, (updated, {"": 0o755}))
mirror_tar = module._release_tar_bytes("a" * 40, (before_entries, {"": 0o755}))
current_path = original
monkeypatch.setattr(
module, "_directory",
lambda _path, _anchor: os.open(current_path, os.O_RDONLY | os.O_DIRECTORY),
)
read_item = module._file_info_at_optional
swapped = False
def swap_after_first_read(descriptor, name):
nonlocal swapped
value = read_item(descriptor, name)
if not swapped:
original.rename(moved)
original.mkdir()
swapped = True
return value
monkeypatch.setattr(module, "_file_info_at_optional", swap_after_first_read)
module._sync_controller_mirror(
prior_tar, updated_tar, mirror_tar, direction="forward"
)
assert swapped and list(original.iterdir()) == []
assert (moved / "controller_paths.py").read_bytes() == b"updated:controller_paths.py"
assert (moved / "mcp_hub_release_store.py").read_bytes() == b"updated:mcp_hub_release_store.py"
current_path = moved
monkeypatch.setattr(module, "_file_info_at_optional", read_item)
module._sync_controller_mirror(
updated_tar, prior_tar, mirror_tar, direction="rollback"
)
assert {path.name: path.read_bytes() for path in moved.iterdir()} == {
name: value for name, (value, _mode) in before_entries.items()
}
@pytest.mark.skipif(os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="private /tmp ownership validation requires root on POSIX")
def test_private_stage_rejects_symlinked_source_directory():
import shutil
import uuid
work = Path("/tmp") / f"itworx-controller-upgrade-{uuid.uuid4().hex[:12]}.Ab3xY7"
source = work / "source"
source.mkdir(parents=True, mode=0o700)
source.chmod(0o700)
work.chmod(0o700)
try:
module._validate_private_stage(source)
source.rename(work / "source-real")
source.symlink_to(work / "source-real", target_is_directory=True)
with pytest.raises(module.VolumeTransitionError, match="private_stage_invalid"):
module._validate_private_stage(source)
finally:
shutil.rmtree(work)
def test_source_compose_preflight_rejects_host_file_reads_before_compose(compose):
_baseline, calls = compose
unsafe = (
b"name: gitea-deploy-runners\nservices:\n deploy-control:\n"
b" image: x\n env_file: /tmp/secret\n"
)
with pytest.raises(module.VolumeTransitionError, match="external_source_invalid"):
module.rendered_model(unsafe, REVISION, IMAGE_ID)
assert calls == []
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)
compile(legacy, "legacy-controller-capture", "exec")
compile(protected, "protected-controller-capture", "exec")
assert "config_root='/opt/gitea-deploy'" in legacy
assert "config_root='/controller-config'" in protected
assert "os.O_NOFOLLOW" in legacy and "os.fstat(fd)" in legacy
assert "open(path,'rb')" not in legacy
def test_prior_release_capture_must_match_protected_git_tree(monkeypatch):
files = module.LEGACY_RELEASE_FILES_2_1_5
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
source_revision = "32a6eece0e3e1276946abdc04196c819daf02e2a"
source_tree = "e0d7e80b79504b964d968e51e0d21c288cac8f0d"
manifest = {
"apiVersion": "itworx.controller-artifact/v1",
"controllerVersion": "2.1.5",
"sourceRepository": "jens/unraid-appops-gateway",
"sourceRevision": source_revision,
"sourceTree": source_tree,
"artifactSha256": module._artifact_digest(entries, files),
"builtAt": "2026-10-01T00:00:00.000Z",
}
entries["controller-release.json"] = (
json.dumps(manifest, separators=(",", ":")).encode(), 0o644
)
identity = module._release_identity(entries)
outputs = {"rev-parse": (source_tree + "\n").encode()}
outputs.update({
f"ops/gitea-deploy-broker/{name}": entries[name][0]
for name in files if name != "run_repopulse_adoption.py"
})
outputs["scripts/run_repopulse_adoption.py"] = entries["run_repopulse_adoption.py"][0]
def run(arguments, **_kwargs):
return outputs[arguments[-1].split(":", 1)[-1]] if arguments[-2] == "show" else outputs["rev-parse"]
monkeypatch.setattr(module, "_run", run)
module._verify_git_source_identity(entries, identity)
outputs["ops/gitea-deploy-broker/deploy.py"] = b"replaced source"
with pytest.raises(module.VolumeTransitionError, match="source_mismatch"):
module._verify_git_source_identity(entries, identity)
@@ -181,60 +181,55 @@ def test_successful_reconciliation_can_be_rolled_back_before_controller_swap(tmp
assert not journal.exists()
def test_upgrade_stops_old_writer_before_locking_cache_and_reconciles_while_stopped():
def test_upgrade_prepares_before_stop_and_reconciles_between_stop_and_activation():
script = UPGRADE_PATH.read_text(encoding="utf-8")
cache_lock = 'flock -x "$cache_lock_fd"'
stop = 'writers_stopped=1\ndocker compose --project-directory "$ROOT" -f "$COMPOSE_FILE" stop "$COMPOSE_SERVICE"'
reconcile = 'python3 "${stage}/reconcile_controller_state.py" \\\n --legacy-root'
backup = 'cp -a -- "$CONTROLLER_DIR" "$backup/controller"'
swap = 'mv -- "$CONTROLLER_DIR" "$previous"'
prepare = 'backup="$(python3 "${work}/${PREPARER_NAME}" prepare \\'
stop = 'python3 "${work}/${PREPARER_NAME}" stop --backup "$backup"'
reconcile = 'python3 "${work}/reconcile_controller_state.py" \\\n --legacy-root'
activate = 'python3 "${work}/${PREPARER_NAME}" activate --backup "$backup"'
assert '/srv/example/appdata/gitea-deploy-runners/deploy.lock' not in script
assert script.index(stop) < script.index(cache_lock)
assert script.index(stop) < script.index(backup)
assert script.index(cache_lock) < script.index(backup)
assert script.index(backup) < script.index(reconcile)
assert script.index(reconcile) < script.index(swap)
assert 'flock -u "$cache_lock_fd"' not in script
assert script.index(prepare) < script.index(stop)
assert script.index(stop) < script.index(reconcile)
assert script.index(reconcile) < script.index(activate)
assert 'flock -x "$promotion_lock_fd"' in script
assert 'mv -- "$CONTROLLER_DIR"' not in script
assert 'cp -a -- "$CONTROLLER_DIR"' not in script
def test_upgrade_retains_and_validates_optional_private_tls_before_stopping_controller():
def test_upgrade_prepares_and_validates_captured_optional_private_tls_before_stopping_controller():
script = UPGRADE_PATH.read_text(encoding="utf-8")
stage_copy = 'cp -a -- "$CONTROLLER_DIR" "${stage}/controller"'
tls_gate = (
'if [[ -e "${stage}/controller/private-tls.json" '
'|| -L "${stage}/controller/private-tls.json" ]]; then\n'
' python3 "${stage}/controller/status_server.py" --check-private-tls-config\n'
'fi'
)
stop = (
'writers_stopped=1\ndocker compose --project-directory "$ROOT" '
'-f "$COMPOSE_FILE" stop "$COMPOSE_SERVICE"'
)
prepare = 'backup="$(python3 "${work}/${PREPARER_NAME}" prepare \\'
stop = 'python3 "${work}/${PREPARER_NAME}" stop --backup "$backup"'
preparer = (ROOT / "scripts/prepare_controller_hub_release_volume.py").read_text(encoding="utf-8")
assert stage_copy in script
assert script.index(stage_copy) < script.index(tls_gate)
assert script.index(tls_gate) < script.index(stop)
assert 'cp -a -- "$CONTROLLER_DIR" "$backup/controller"' in script
assert "_validate_previous_private_tls(stage_root)" in preparer
assert script.index(prepare) < script.index(stop)
def test_upgrade_rolls_back_state_before_restoring_or_starting_the_old_writer():
script = UPGRADE_PATH.read_text(encoding="utf-8")
restore = script[script.index("restore_previous() {") : script.index("cleanup() {")]
stop = 'docker compose --project-directory "$ROOT" -f "$COMPOSE_FILE" stop "$COMPOSE_SERVICE"'
stop = 'if ! python3 "${work}/${PREPARER_NAME}" rollback --backup "$backup" --stop-only; then'
rollback = '--rollback'
restore_controller = 'mv -- "$previous" "$CONTROLLER_DIR"'
start = 'docker compose --project-directory "$ROOT" -f "$COMPOSE_FILE" up -d --force-recreate "$COMPOSE_SERVICE"'
restore_controller = 'if ! python3 "${work}/${PREPARER_NAME}" rollback --backup "$backup"; then'
assert restore.index(stop) < restore.index(rollback)
assert restore.index(rollback) < restore.index(restore_controller)
assert restore.index(restore_controller) < restore.index(start)
assert "the old writer will remain stopped" in script
reconcile = 'python3 "${stage}/reconcile_controller_state.py" \\\n --legacy-root'
assert script.index("reconciliation_active=1", script.index("writers_stopped=1")) < script.index(reconcile)
assert "controller remains stopped" in script
assert 'mv -- "$previous"' not in restore
assert 'docker compose --project-directory "$ROOT"' not in restore
reconcile = 'python3 "${work}/reconcile_controller_state.py"'
stopped = script.index("writers_stopped=1", script.index(stop))
reconcile_index = script.index(reconcile, stopped)
activate_index = script.index(
'python3 "${work}/${PREPARER_NAME}" activate --backup "$backup"',
reconcile_index,
)
assert stopped < reconcile_index < activate_index
assert '[[ -f "$migration_journal" && ! -L "$migration_journal" ]]' in restore
assert '[[ -e "$migration_journal" || -L "$migration_journal" ]]' in restore
+432
View File
@@ -2226,6 +2226,96 @@ def test_managed_repopulse_render_accepts_only_its_read_only_source_mount(
deploy_module.validate_managed_v1_runtime(project, "a" * 40, "jens/repopulse")
def test_managed_appdata_roots_are_fixed_and_bound_to_each_registration(
deploy_module, monkeypatch, tmp_path
):
legacy_root = tmp_path / "legacy-appdata"
managed_root = tmp_path / "managed-volume" / "_data"
monkeypatch.setattr(deploy_module, "LEGACY_MANAGED_APPCONFIG_ROOT", legacy_root)
monkeypatch.setattr(deploy_module, "MANAGED_APPCONFIG_ROOT", legacy_root)
monkeypatch.setattr(deploy_module, "MANAGED_APPCONFIG_VOLUME_ROOT", managed_root)
monkeypatch.setattr(deploy_module, "managed_appdata_volume_initialized", lambda: False)
assert deploy_module._registered_managed_appdata_root({}) == legacy_root
assert deploy_module._registered_managed_appdata_root(
{"managed_appdata_root": legacy_root.as_posix()}
) == legacy_root
with pytest.raises(deploy_module.DeployError, match="not initialized and available"):
deploy_module._registered_managed_appdata_root(
{"managed_appdata_root": managed_root.as_posix()}
)
with pytest.raises(deploy_module.DeployError, match="fixed registered roots"):
deploy_module._registered_managed_appdata_root(
{"managed_appdata_root": (tmp_path / "arbitrary").as_posix()}
)
monkeypatch.setattr(deploy_module, "managed_appdata_volume_initialized", lambda: True)
assert deploy_module._registered_managed_appdata_root(
{"managed_appdata_root": managed_root.as_posix()}
) == managed_root
for root, host in ((legacy_root, "legacy.example"), (managed_root, "managed.example")):
env_path = root / "demo" / "deploy.env"
env_path.parent.mkdir(parents=True)
env_path.write_text(f"APPOPS_PUBLIC_URL=https://{host}\n", encoding="utf-8")
if os.name != "nt":
env_path.chmod(0o600)
project = {
"appops_managed_target": True,
"managed_plan_version": "itworx.deployment/v1",
"managed_profile": "internal-webapp",
"managed_application_id": "demo",
"managed_runtime_environment_keys": ["APPOPS_PUBLIC_URL"],
"env_source": (legacy_root / "demo" / "deploy.env").as_posix(),
}
assert deploy_module.managed_runtime_environment(project) == {
"APPOPS_PUBLIC_URL": "https://legacy.example"
}
project["managed_appdata_root"] = managed_root.as_posix()
project["env_source"] = (managed_root / "demo" / "deploy.env").as_posix()
assert deploy_module.managed_runtime_environment(project) == {
"APPOPS_PUBLIC_URL": "https://managed.example"
}
def test_managed_project_path_is_derived_from_its_stored_appdata_root(
deploy_module, monkeypatch
):
monkeypatch.setattr(deploy_module, "managed_appdata_volume_initialized", lambda: True)
monkeypatch.setattr(deploy_module, "git_path_exists", lambda *_args: True)
project = {
"containers": ["demo"],
"source_dir": (deploy_module.EXACT_SOURCE_ROOT / "demo").as_posix(),
"exact_source": True,
"compose_dir": ".",
"compose_files": ["compose.yml"],
"compose_project": "demo",
"managed_plan_version": "itworx.deployment/v1",
"managed_application_id": "demo",
"managed_appdata_path": "/srv/example/appdata/demo",
"managed_image_repository": "demo",
"managed_image_tag": "production",
"managed_proxy_network": "itworx-proxy",
"managed_internal_port": 8000,
"appops_deployment_secret_file": "jens__demo",
}
deploy_module.validate_project_config(Path("repo.git"), project, "a" * 40)
managed = {
**project,
"managed_appdata_root": deploy_module.MANAGED_APPCONFIG_VOLUME_ROOT.as_posix(),
"managed_appdata_path": (
deploy_module.MANAGED_APPCONFIG_VOLUME_ROOT / "demo"
).as_posix(),
}
deploy_module.validate_project_config(Path("repo.git"), managed, "a" * 40)
remapped = {**managed, "managed_appdata_path": "/srv/example/appdata/demo"}
with pytest.raises(deploy_module.DeployError, match="not derived"):
deploy_module.validate_project_config(Path("repo.git"), remapped, "a" * 40)
def test_repopulse_project_registry_requires_its_fixed_build_identity(
deploy_module, monkeypatch
):
@@ -4582,6 +4672,25 @@ def _appops_plan(revision: str) -> dict[str, object]:
}
def test_appops_plan_root_must_match_the_registered_target_root(status_module):
project = _appops_project()
plan = _appops_plan("a" * 40)
assert status_module._appops_plan({"jens/repo-a": project}, plan) == (
"jens/repo-a", "a" * 40, project,
)
project["managed_appdata_root"] = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
project["managed_appdata_path"] = "/var/lib/docker/volumes/itworx-managed-appdata/_data/repo-a"
alternate_plan = {**plan, "trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data"}
assert status_module._appops_plan({"jens/repo-a": project}, alternate_plan) == (
"jens/repo-a", "a" * 40, project,
)
assert status_module._appops_plan({"jens/repo-a": project}, plan) is None
drifted = {**project, "managed_appdata_path": "/srv/example/appdata/repo-a"}
assert status_module._appops_plan({"jens/repo-a": drifted}, alternate_plan) is None
def _write_appops_secret(root: Path, name: str, value: bytes) -> None:
root.mkdir(mode=0o700)
root.chmod(0o700)
@@ -5838,6 +5947,7 @@ def _controller_enrollment_payload(*, credential: bool = False) -> dict[str, obj
"profile": "internal-webapp-oidc",
"container_name": "wsbc-portal",
"compose_file": ".itworx/deployment.yml",
"trusted_appdata_root": "/srv/example/appdata",
"appdata_path": "/srv/example/appdata/wsbc-portal",
"data_path": "/srv/example/appdata/wsbc-portal/data",
"runtime_uid": 1654,
@@ -5871,6 +5981,7 @@ def _repopulse_controller_enrollment_payload(*, credential: bool = False) -> dic
"profile": "repopulse-authenticated",
"container_name": "repopulse",
"compose_file": ".itworx/deployment.yml",
"trusted_appdata_root": "/srv/example/appdata",
"appdata_path": "/srv/example/appdata/repopulse",
"data_path": "/srv/example/appdata/repopulse/data",
"runtime_uid": 10001,
@@ -5929,8 +6040,10 @@ def test_controller_repopulse_runtime_seal_fixes_gitea_url_and_hides_values(stat
env_path = app_root / "deploy.managed.env"
env_path.write_text("", encoding="utf-8")
monkeypatch.setattr(status_module, "TRUSTED_APPCONFIG_ROOT", appdata_root)
monkeypatch.setattr(status_module, "LEGACY_APPDATA_ROOT", appdata_root)
monkeypatch.setattr(status_module, "EXACT_SOURCE_ROOT", tmp_path / "sources")
payload = _repopulse_controller_enrollment_payload(credential=True)
payload["trusted_appdata_root"] = appdata_root.as_posix()
payload["appdata_path"] = app_root.as_posix()
payload["data_path"] = (app_root / "data").as_posix()
enrollment = status_module._controller_enrollment(payload, require_credential=True)
@@ -6106,6 +6219,76 @@ def test_controller_enrollment_contract_rejects_arbitrary_compose_and_paths(stat
assert status_module._controller_enrollment(unsafe, require_credential=True) is None
def test_controller_enrollment_derives_paths_from_the_selected_fixed_root(status_module, monkeypatch):
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
monkeypatch.setattr(status_module, "TRUSTED_APPCONFIG_ROOT", Path(managed_root))
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda root: root.as_posix() == managed_root)
alternate = _controller_enrollment_payload()
alternate["trusted_appdata_root"] = managed_root
alternate["appdata_path"] = managed_root + "/wsbc-portal"
alternate["data_path"] = managed_root + "/wsbc-portal/data"
enrollment = status_module._controller_enrollment(alternate, require_credential=False)
assert enrollment is not None
project = status_module._enrollment_project(enrollment)
assert project["managed_appdata_root"] == managed_root
assert project["managed_appdata_path"] == managed_root + "/wsbc-portal"
unsafe_path = {**alternate, "data_path": "/srv/example/appdata/wsbc-portal/data"}
assert status_module._controller_enrollment(unsafe_path, require_credential=False) is None
unsupported_root = {**alternate, "trusted_appdata_root": "/srv/example/other"}
assert status_module._controller_enrollment(unsupported_root, require_credential=False) is None
def test_controller_accepts_legacy_requests_after_managed_marker_is_initialized(status_module, monkeypatch):
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
monkeypatch.setattr(status_module, "_refresh_appdata_root_selection", lambda: Path(managed_root))
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda root: root.as_posix() == managed_root)
legacy = _controller_enrollment_payload()
assert legacy["trusted_appdata_root"] == "/srv/example/appdata"
enrollment = status_module._controller_enrollment(legacy, require_credential=False)
assert enrollment is not None
assert enrollment["trusted_appdata_root"] == "/srv/example/appdata"
assert "managed_appdata_root" not in status_module._enrollment_project(enrollment)
def test_controller_accepts_legacy_v1_payload_without_root_field(status_module):
legacy = _controller_enrollment_payload()
legacy.pop("trusted_appdata_root")
enrollment = status_module._controller_enrollment(legacy, require_credential=False)
assert enrollment is not None
assert enrollment["trusted_appdata_root"] == "/srv/example/appdata"
assert "managed_appdata_root" not in status_module._enrollment_project(enrollment)
def test_repopulse_enrollment_remains_bound_to_legacy_appdata_root(status_module, monkeypatch):
monkeypatch.setattr(status_module, "TRUSTED_APPCONFIG_ROOT", Path("/var/lib/docker/volumes/itworx-managed-appdata/_data"))
payload = _repopulse_controller_enrollment_payload()
payload["trusted_appdata_root"] = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
payload["appdata_path"] = "/var/lib/docker/volumes/itworx-managed-appdata/_data/repopulse"
payload["data_path"] = "/var/lib/docker/volumes/itworx-managed-appdata/_data/repopulse/data"
assert status_module._controller_enrollment(payload, require_credential=False) is None
def test_fixed_repopulse_profile_stays_legacy_with_managed_marker(status_module, monkeypatch):
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
monkeypatch.setattr(status_module, "_refresh_appdata_root_selection", lambda: Path(managed_root))
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda root: root.as_posix() == managed_root)
enrollment = status_module._controller_enrollment(
_repopulse_controller_enrollment_payload(),
require_credential=False,
)
assert enrollment is not None
assert enrollment["trusted_appdata_root"] == "/srv/example/appdata"
def test_controller_runtime_seal_accepts_only_profile_owned_keys(status_module, tmp_path, monkeypatch):
appdata_root = tmp_path / "appdata"
app_root = appdata_root / "wsbc-portal"
@@ -6113,8 +6296,10 @@ def test_controller_runtime_seal_accepts_only_profile_owned_keys(status_module,
env_path = app_root / "deploy.env"
env_path.write_text("", encoding="utf-8")
monkeypatch.setattr(status_module, "TRUSTED_APPCONFIG_ROOT", appdata_root)
monkeypatch.setattr(status_module, "LEGACY_APPDATA_ROOT", appdata_root)
monkeypatch.setattr(status_module, "EXACT_SOURCE_ROOT", tmp_path / "sources")
payload = _controller_enrollment_payload(credential=True)
payload["trusted_appdata_root"] = appdata_root.as_posix()
payload["appdata_path"] = app_root.as_posix()
payload["data_path"] = (app_root / "data").as_posix()
enrollment = status_module._controller_enrollment(payload, require_credential=True)
@@ -6154,6 +6339,7 @@ def test_controller_enrollment_replay_requires_the_existing_target_credential(st
projects_path.write_text(json.dumps({"jens/windows-support-bundle-collector": project}), encoding="utf-8")
monkeypatch.setattr(status_module, "PROJECTS_PATH", projects_path)
monkeypatch.setattr(status_module, "APPOPS_DEPLOYMENT_SECRET_ROOT", secrets_root)
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda _root: True)
enrollment["target_credential"] = "y" * 64
status, _evidence = status_module._enroll_controller_target(enrollment)
@@ -7316,3 +7502,249 @@ def test_workmesh_backup_receipt_requires_bound_manifest_and_verified_restore_by
assert status_module._verified_workmesh_backup_receipt(
request_id, candidate, preflight, inputs
) is None
def _managed_storage_http_request(
status_module,
server,
*,
body: bytes,
nonce: str,
secret: bytes | None,
signature: str | None = None,
signed_path: str | None = None,
):
path = status_module.MANAGED_APPDATA_INITIALIZE_PATH
timestamp = str(int(status_module.time.time()))
headers = {"Content-Type": "application/json", "Content-Length": str(len(body))}
if nonce:
headers["X-ITWorx-Deployment-Nonce"] = nonce
if timestamp:
headers["X-ITWorx-Deployment-Timestamp"] = timestamp
if signature is None and secret is not None:
signature = status_module._request_signature(
secret,
method="POST",
path=signed_path or path,
timestamp=timestamp,
nonce=nonce,
raw_body=body,
)
if signature is not None:
headers["X-ITWorx-Deployment-Signature"] = signature
connection = http.client.HTTPConnection(*server.server_address, timeout=2)
connection.request("POST", path, body=body, headers=headers)
response = connection.getresponse()
status = response.status
payload = json.loads(response.read())
connection.close()
return status, payload, headers
def test_managed_appdata_initializer_http_requires_exact_signed_one_time_request(
status_module, tmp_path, monkeypatch
):
secret = b"managed-storage-bootstrap-" + b"x" * 48
secret_path = tmp_path / "bootstrap"
secret_path.write_bytes(secret)
secret_path.chmod(0o600)
nonce_root = tmp_path / "nonces"
monkeypatch.setattr(status_module, "APPOPS_ENROLLMENT_BOOTSTRAP_SECRET_FILE", secret_path)
monkeypatch.setattr(status_module, "APPOPS_NONCE_ROOT", nonce_root)
monkeypatch.setattr(
status_module.Handler,
"_managed_storage_initialization_prerequisites",
lambda _handler: True,
)
initialized = []
monkeypatch.setattr(
status_module,
"ensure_managed_appdata_root",
lambda: initialized.append(True) or "initialized",
)
monkeypatch.setattr(
status_module,
"_refresh_appdata_root_selection",
lambda: status_module.MANAGED_APPDATA_ROOT,
)
server = status_module.BoundedThreadingHTTPServer(
("127.0.0.1", 0), status_module.Handler
)
serving = threading.Thread(target=server.serve_forever, daemon=True)
serving.start()
body = json.dumps(
{
"apiVersion": status_module.MANAGED_APPDATA_INITIALIZE_API_VERSION,
"target": status_module.MANAGED_APPDATA_INITIALIZE_TARGET,
},
separators=(",", ":"),
).encode("utf-8")
try:
success_status, success_payload, success_headers = _managed_storage_http_request(
status_module,
server,
body=body,
nonce="a" * 32,
secret=secret,
)
assert success_status == 200
assert success_payload == {
"status": "initialized",
"trustedAppdataRoot": status_module.MANAGED_APPDATA_ROOT.as_posix(),
"backupLocation": "dockerRootDir",
}
assert initialized == [True]
assert secret.decode("ascii") not in json.dumps(success_payload)
replay = http.client.HTTPConnection(*server.server_address, timeout=2)
replay.request(
"POST",
status_module.MANAGED_APPDATA_INITIALIZE_PATH,
body=body,
headers=success_headers,
)
replay_response = replay.getresponse()
replay_payload = json.loads(replay_response.read())
replay.close()
assert replay_response.status == 403
assert replay_payload == {"status": "authentication_rejected"}
assert initialized == [True]
invalid_requests = [
_managed_storage_http_request(
status_module,
server,
body=body,
nonce="b" * 32,
secret=secret,
signature="0" * 64,
),
_managed_storage_http_request(
status_module,
server,
body=body,
nonce="c" * 32,
secret=secret,
signed_path="/v1/appops/enrollments/capabilities",
),
_managed_storage_http_request(
status_module,
server,
body=body,
nonce="d" * 32,
secret=None,
),
]
assert [status for status, _payload, _headers in invalid_requests] == [403, 403, 403]
assert initialized == [True]
for index, payload in enumerate(
[
{
"apiVersion": status_module.MANAGED_APPDATA_INITIALIZE_API_VERSION,
"target": "tower/other-controller",
},
{
"apiVersion": status_module.MANAGED_APPDATA_INITIALIZE_API_VERSION,
"target": status_module.MANAGED_APPDATA_INITIALIZE_TARGET,
"path": "C:/private/sentinel",
},
],
start=1,
):
raw_body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
rejected_status, rejected_payload, _headers = _managed_storage_http_request(
status_module,
server,
body=raw_body,
nonce=f"{index:032x}",
secret=secret,
)
assert rejected_status == 400
assert rejected_payload == {"status": "invalid_managed_storage_request"}
assert "C:/private/sentinel" not in json.dumps(rejected_payload)
oversized_status, oversized_payload, _headers = _managed_storage_http_request(
status_module,
server,
body=b"x" * 1025,
nonce="e" * 32,
secret=None,
)
assert oversized_status == 413
assert oversized_payload == {"status": "invalid_body_size"}
assert initialized == [True]
finally:
server.shutdown()
server.server_close()
serving.join(timeout=2)
def test_managed_appdata_initializer_http_prerequisite_and_refusal_are_sanitized(
status_module, tmp_path, monkeypatch
):
secret = b"managed-storage-bootstrap-" + b"y" * 48
secret_path = tmp_path / "bootstrap"
secret_path.write_bytes(secret)
secret_path.chmod(0o600)
monkeypatch.setattr(status_module, "APPOPS_ENROLLMENT_BOOTSTRAP_SECRET_FILE", secret_path)
monkeypatch.setattr(status_module, "APPOPS_NONCE_ROOT", tmp_path / "nonces")
prerequisites = {"ready": False}
monkeypatch.setattr(
status_module.Handler,
"_managed_storage_initialization_prerequisites",
lambda _handler: prerequisites["ready"],
)
calls = []
monkeypatch.setattr(
status_module,
"ensure_managed_appdata_root",
lambda: calls.append(True) or "initialized",
)
monkeypatch.setattr(
status_module,
"_refresh_appdata_root_selection",
lambda: status_module.MANAGED_APPDATA_ROOT,
)
server = status_module.BoundedThreadingHTTPServer(
("127.0.0.1", 0), status_module.Handler
)
serving = threading.Thread(target=server.serve_forever, daemon=True)
serving.start()
body = json.dumps(
{
"apiVersion": status_module.MANAGED_APPDATA_INITIALIZE_API_VERSION,
"target": status_module.MANAGED_APPDATA_INITIALIZE_TARGET,
},
separators=(",", ":"),
).encode("utf-8")
try:
status, payload, _headers = _managed_storage_http_request(
status_module, server, body=body, nonce="f" * 32, secret=secret
)
assert status == 503
assert payload == {"status": "managed_storage_prerequisite_unavailable"}
assert calls == []
prerequisites["ready"] = True
def refuse_with_sensitive_diagnostic():
raise status_module.UnsafeManagedAppdataRoot(
"secret/path sentinel C:/private/appdata"
)
monkeypatch.setattr(
status_module, "ensure_managed_appdata_root", refuse_with_sensitive_diagnostic
)
status, payload, _headers = _managed_storage_http_request(
status_module, server, body=body, nonce="1" * 32, secret=secret
)
assert status == 409
assert payload == {"status": "managed_storage_initialization_refused"}
assert "secret/path sentinel" not in json.dumps(payload)
assert "C:/private/appdata" not in json.dumps(payload)
assert secret.decode("ascii") not in json.dumps(payload)
finally:
server.shutdown()
server.server_close()
serving.join(timeout=2)
+170 -1
View File
@@ -74,12 +74,14 @@ def _trust():
class _Broker:
appdata_root = "/srv/example/appdata"
def is_configured(self):
return True
def enrollment_capabilities(self, *, secret):
assert secret == "b" * 64
return {"status": "ready"}
return {"status": "ready", "trustedAppdataRoot": self.appdata_root}
def enrollment_preflight(self, enrollment, *, secret):
assert secret == "b" * 64
@@ -251,6 +253,25 @@ def test_enrollment_schema_rejects_plaintext_secrets_and_untrusted_topology():
raise AssertionError("wildcard hostname must be rejected")
def test_appdata_root_defaults_to_legacy_and_allows_only_the_fixed_managed_root():
plan = _plan()
assert plan.trusted_appdata_root == "/srv/example/appdata"
assert plan.appdata_path == "/srv/example/appdata/wsbc-portal"
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
managed = _plan(trusted_appdata_root=managed_root)
assert managed.appdata_path == managed_root + "/wsbc-portal"
plan_payload = _plan().model_dump()
plan_payload["trusted_appdata_root"] = "/srv/example/other"
trust_payload = _trust().model_dump()
trust_payload["trusted_appdata_root"] = "/srv/example/other"
with pytest.raises(ValueError):
EnrollmentPlanV1.model_validate(plan_payload)
with pytest.raises(ValueError):
EnrollmentTrustConfigV1.model_validate(trust_payload)
def test_repopulse_profile_accepts_only_its_fixed_authenticated_target():
plan = _repopulse_plan()
assert plan.application_id == "repopulse"
@@ -291,6 +312,34 @@ def test_repopulse_preflight_requires_existing_secret_and_trusted_npm_access_lis
assert session.exec(select(EnrollmentJob)).all() == []
def test_repopulse_legacy_profile_remains_usable_when_new_target_root_is_managed(tmp_path):
class RecordingBroker(_RepoPulseBroker):
def __init__(self):
super().__init__()
self.preflight_payloads = []
def enrollment_preflight(self, enrollment, *, secret):
self.preflight_payloads.append(dict(enrollment))
return super().enrollment_preflight(enrollment, secret=secret)
broker = RecordingBroker()
with _session(tmp_path) as session:
_configure(session)
set_secret(session, "REPOPULSE_GITEA_TOKEN", "opaque-gitea-token")
EnrollmentPlane.set_trust_config(
session,
_trust().model_copy(update={
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
}),
)
result = _plane(broker=broker, npm=_RepoPulseNpm()).preflight(session, _repopulse_plan())
assert result["status"] == "ready"
assert "trusted_appdata_root_mismatch" not in result["blockers"]
assert broker.preflight_payloads[0]["trusted_appdata_root"] == "/srv/example/appdata"
def test_repopulse_enrollment_seals_exact_runtime_values_without_exposing_secrets(tmp_path):
with _session(tmp_path) as session:
_configure(session)
@@ -347,6 +396,126 @@ def test_capabilities_require_a_live_bootstrap_controller_contract(tmp_path):
assert result["controller"]["status"] == "ready"
def test_capabilities_block_when_controller_root_differs_from_operator_trust(tmp_path):
class AlternateRootBroker(_Broker):
appdata_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
with _session(tmp_path) as session:
_configure(session)
result = _plane(broker=AlternateRootBroker()).capabilities(session)
assert result["targetEnrollment"]["status"] == "unavailable"
assert result["controller"]["status"] == "unavailable"
assert result["controller"]["reason"] == "trusted_appdata_root_mismatch"
def test_capabilities_treat_roots_as_independent_availability_not_global_selection(tmp_path):
legacy_root = "/srv/example/appdata"
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
class MultiRootBroker(_Broker):
def enrollment_capabilities(self, *, secret):
assert secret == "b" * 64
return {
"status": "ready",
"trustedAppdataRoot": managed_root,
"availableTrustedAppdataRoots": [
{"root": legacy_root, "status": "ready", "reason": ""},
{"root": managed_root, "status": "ready", "reason": ""},
],
"managedStorage": {"status": "ready", "initializationAvailable": False},
}
with _session(tmp_path) as session:
_configure(session)
plane = _plane(broker=MultiRootBroker())
legacy = plane.capabilities(session)
assert legacy["targetEnrollment"]["status"] == "ready"
assert legacy["managedStorage"]["status"] == "not_selected"
assert legacy["availableTrustedAppdataRoots"] == [
{"root": legacy_root, "status": "ready"},
{"root": managed_root, "status": "ready"},
]
EnrollmentPlane.set_trust_config(
session,
_trust().model_copy(update={
"trusted_appdata_root": managed_root,
"managed_appdata_backup_acknowledged": True,
}),
)
managed = plane.capabilities(session)
assert managed["targetEnrollment"]["status"] == "ready"
assert managed["managedStorage"]["status"] == "ready"
assert managed["availableTrustedAppdataRoots"] == [
{"root": legacy_root, "status": "ready"},
{"root": managed_root, "status": "ready"},
]
def test_capabilities_fail_closed_and_hide_unrecognized_controller_roots(tmp_path):
class MalformedRootsBroker(_Broker):
def enrollment_capabilities(self, *, secret):
return {
"status": "ready",
"trustedAppdataRoot": "/srv/example/appdata",
"availableTrustedAppdataRoots": [
{"root": "/srv/example/appdata", "status": "ready"},
{"root": "/etc/private", "status": "ready", "reason": "sensitive controller path"},
],
}
with _session(tmp_path) as session:
_configure(session)
result = _plane(broker=MalformedRootsBroker()).capabilities(session)
assert result["targetEnrollment"]["status"] == "unavailable"
assert result["availableTrustedAppdataRoots"] == [
{"root": "/srv/example/appdata", "status": "unavailable"},
{"root": "/var/lib/docker/volumes/itworx-managed-appdata/_data", "status": "unavailable"},
]
assert "/etc/private" not in repr(result)
assert "sensitive controller path" not in repr(result)
def test_enrollment_preflight_binds_root_to_trust_and_controller_payload(tmp_path):
class RecordingBroker(_Broker):
def __init__(self):
self.preflight_payloads = []
def enrollment_preflight(self, enrollment, *, secret):
self.preflight_payloads.append(dict(enrollment))
return super().enrollment_preflight(enrollment, secret=secret)
broker = RecordingBroker()
with _session(tmp_path) as session:
_configure(session)
plane = _plane(broker=broker)
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
alternate = _plan(trusted_appdata_root=managed_root)
blocked = plane.preflight(session, alternate)
assert blocked["status"] == "blocked"
assert "trusted_appdata_root_mismatch" in blocked["blockers"]
assert broker.preflight_payloads == []
EnrollmentPlane.set_trust_config(
session,
_trust().model_copy(update={
"trusted_appdata_root": managed_root,
"managed_appdata_backup_acknowledged": True,
}),
)
payload = plane._controller_payload(
alternate,
enrollment_id="enr-00000000000000000000",
)
assert payload["trusted_appdata_root"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data"
assert payload["appdata_path"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data/wsbc-portal"
assert payload["data_path"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data/wsbc-portal/data"
def test_enrollment_creates_write_only_credentials_exact_policies_and_is_idempotent(tmp_path):
with _session(tmp_path) as session:
_configure(session)
+330
View File
@@ -0,0 +1,330 @@
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
@@ -0,0 +1,273 @@
from __future__ import annotations
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
from backend.app.db.models import AppSetting, AuditEvent, JobRun, Policy
from backend.app.services.action_execution import (
approval_execution_supported,
execute_governed_action,
mcp_execution_supported,
)
from backend.app.services.action_registry import (
ACTION_RISK,
ACTIONS,
action_default_target,
)
from backend.app.services.policy_engine import evaluate_policy
ACTION = "controller.managed-storage.initialize"
TARGET = "tower/deployment-controller"
def test_managed_storage_initialization_is_typed_high_risk_and_fixed_target():
assert ACTION in ACTIONS
assert ACTION_RISK[ACTION] == "high"
assert action_default_target(ACTION) == TARGET
assert not approval_execution_supported(ACTION)
assert mcp_execution_supported(ACTION)
def test_managed_storage_policy_defaults_to_blocked_and_requires_exact_manual_allow(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'managed-storage-policy.db'}")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "blocked"
session.add(Policy(
id="managed-storage-wildcard",
action=ACTION,
target="*",
voice="blocked",
manual="allowed",
risk="high",
))
session.commit()
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "blocked"
session.add(Policy(
id="managed-storage-exact",
action=ACTION,
target=TARGET,
voice="allowed",
manual="allowed",
risk="high",
))
session.commit()
assert evaluate_policy(session, ACTION, TARGET, "manual")["decision"] == "allowed"
assert evaluate_policy(session, ACTION, "tower/other-controller", "manual")["decision"] == "blocked"
assert evaluate_policy(session, ACTION, TARGET, "voice")["decision"] == "blocked"
engine.dispose()
def _executor_engine(tmp_path):
engine = create_engine(f"sqlite:///{tmp_path / 'managed-storage-executor.db'}")
SQLModel.metadata.create_all(engine)
return engine
def _managed_trust():
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
return EnrollmentTrustConfigV1.model_validate({
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
})
def _execute_initializer(session):
return execute_governed_action(
session,
action=ACTION,
target=TARGET,
source="chatgpt-mcp",
risk="high",
reason="Operator explicitly selected managed storage and acknowledged its backup scope.",
original_policy_decision="allowed",
)
@pytest.mark.parametrize("trust_state", ["missing", "legacy"])
def test_managed_storage_executor_refuses_missing_selection_or_ack_without_broker_call(
tmp_path, monkeypatch, trust_state
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
engine = _executor_engine(tmp_path)
if trust_state == "legacy":
legacy = _managed_trust().model_copy(update={
"trusted_appdata_root": "/srv/example/appdata",
"managed_appdata_backup_acknowledged": False,
})
with Session(engine) as setup:
setup.add(AppSetting(
key="deployment_enrollment:trust",
value_json=legacy.model_dump(mode="json"),
is_secret=False,
))
setup.commit()
monkeypatch.setattr(
DeploymentBrokerClient,
"initialize_managed_appdata",
lambda *_args, **_kwargs: pytest.fail("broker must not be called without managed trust opt-in"),
)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "blocked"
assert result["auditId"]
assert session.exec(select(JobRun)).all() == []
event = session.exec(select(AuditEvent)).one()
assert event.result == "blocked"
assert event.redacted_metadata == {"executor": ACTION, "trustOptIn": False}
engine.dispose()
def test_managed_storage_executor_requires_bootstrap_secret_without_broker_call(tmp_path, monkeypatch):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
DeploymentBrokerClient,
"initialize_managed_appdata",
lambda *_args, **_kwargs: pytest.fail("broker must not be called without bootstrap secret"),
)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "failed"
assert result["auditId"]
assert session.exec(select(JobRun)).all() == []
event = session.exec(select(AuditEvent)).one()
assert event.result == "failed"
assert event.redacted_metadata == {"executor": ACTION, "secretMaterialReturned": "[REDACTED]"}
engine.dispose()
@pytest.mark.parametrize("status", ["initialized", "already_initialized"])
def test_managed_storage_executor_records_durable_success_without_secret_material(
tmp_path, monkeypatch, status
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
broker_calls = []
def initialize(_client, *, secret):
broker_calls.append(secret)
return {
"status": status,
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
}
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", initialize)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is True
assert result["status"] == "success"
assert result["jobId"] and result["auditId"]
assert broker_calls == ["temporary-bootstrap-secret"]
with Session(engine) as session:
job = session.get(JobRun, result["jobId"])
event = session.get(AuditEvent, result["auditId"])
assert job is not None and job.status == "succeeded"
assert event is not None and event.result == "success"
assert event.after_state == {
"status": status,
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
}
assert event.redacted_metadata["secretMaterialReturned"] == "[REDACTED]"
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "temporary-bootstrap-secret" not in repr(result)
engine.dispose()
@pytest.mark.parametrize(
"broker_result",
[
{"status": "initialized", "trustedAppdataRoot": "/tmp/wrong-root", "backupLocation": "dockerRootDir"},
{"status": "initialized", "trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data", "backupLocation": "external-share"},
{
"status": "secret-sentinel-/var/lib/private",
"trustedAppdataRoot": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"backupLocation": "dockerRootDir",
},
],
ids=["wrong-root", "wrong-backup-location", "untrusted-status"],
)
def test_managed_storage_executor_fails_closed_on_mismatched_broker_attestation(
tmp_path, monkeypatch, broker_result
):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", lambda *_args, **_kwargs: broker_result)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["result"] == "failed"
assert "temporary-bootstrap-secret" not in repr(result)
assert "/tmp/wrong-root" not in repr(result)
assert "secret-sentinel-/var/lib/private" not in repr(result)
event = session.get(AuditEvent, result["auditId"])
assert event is not None and event.result == "failed"
assert event.after_state["trustedAppdataRoot"] is None
assert event.after_state["backupLocation"] is None
if broker_result["status"].startswith("secret-sentinel"):
assert event.after_state["status"] == "refused"
assert "/tmp/wrong-root" not in repr(event.model_dump())
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "secret-sentinel-/var/lib/private" not in repr(event.model_dump())
job = session.get(JobRun, result["jobId"])
assert job is not None and job.status == "failed"
assert "secret-sentinel-/var/lib/private" not in repr(job.model_dump())
engine.dispose()
def test_managed_storage_executor_sanitizes_broker_exception(tmp_path, monkeypatch):
from backend.app.services.deployment_broker import DeploymentBrokerClient
from backend.app.services.enrollment_plane import EnrollmentPlane
engine = _executor_engine(tmp_path)
monkeypatch.setattr(EnrollmentPlane, "get_trust_config", staticmethod(lambda _session: _managed_trust()))
monkeypatch.setattr("backend.app.services.secret_store.get_secret", lambda *_args, **_kwargs: "temporary-bootstrap-secret")
def raise_broker_error(*_args, **_kwargs):
raise RuntimeError("temporary-bootstrap-secret failed under /var/lib/private")
monkeypatch.setattr(DeploymentBrokerClient, "initialize_managed_appdata", raise_broker_error)
with Session(engine) as session:
result = _execute_initializer(session)
assert result["success"] is False
assert result["message"] == "Managed storage initialization was refused or unavailable."
assert "temporary-bootstrap-secret" not in repr(result)
assert "/var/lib/private" not in repr(result)
event = session.get(AuditEvent, result["auditId"])
assert event is not None and event.result == "failed"
assert "temporary-bootstrap-secret" not in repr(event.model_dump())
assert "/var/lib/private" not in repr(event.model_dump())
engine.dispose()
@@ -61,8 +61,8 @@ def test_reconciliation_is_fixed_to_mcp_hub_and_reconciles_only_verified_failed_
assert create[2]["json"]["AttachStderr"] is False
script = command[2]
assert "/opt/gitea-deploy/projects.json" in script
assert "/mnt/cache/appdata/gitea-deploy-runners/controller/projects.json" in script
assert "/controller-config/projects.json" in script
assert "/controller-hub-releases/controller-config/projects.json" in script
assert "os.path.samefile(registry_path, host_registry_path)" in script
assert "/mnt/cache/appdata/gitea-deploy-runners/state" in script
assert "/srv/example/appdata/gitea-deploy-runners/deploy.lock" not in script
@@ -102,7 +102,9 @@ def test_reconciliation_proves_complete_live_compose_topology_and_preserves_actu
assert 'not runtime_state.get("Running")' in script
assert 'health in {"starting", "unhealthy"}' in script
assert 'image_id = str(container.get("Image") or "")' in script
assert 'artifacts = {"containers": container_artifacts, "compose_sha256": ""}' in script
assert '"containers": container_artifacts' in script
assert '"compose_sha256": ""' in script
assert '"topology_contract": topology_contract' in script
assert '"live_legacy_provenance": True' in script
assert '"live_artifacts": artifacts' in script
assert '"live_revision": recorded_live' in script
@@ -123,6 +125,23 @@ def test_reconciliation_recognizes_only_the_fixed_mcp_hub_oci_source_as_owned():
assert '"live_revision": attempt_revision' not in script
def test_reconciliation_requires_the_controller_17_service_topology_migration():
result = McpHubDeploymentReconciliation(FakePortainer(exit_code=56)).run()
assert result == {
"status": "topology_migration_required",
"repository": "jens/itworx_mcp_hub",
"composeProject": "itworx-mcp-hub",
"reason": "MCP Hub 15-to-17 topology migration requires its guarded protected-main deployment",
}
fake = FakePortainer()
McpHubDeploymentReconciliation(fake).run()
script = fake.calls[0][2]["json"]["Cmd"][2]
assert 'topology_contract = "itworx.mcp-hub-topology/17-v1"' in script
assert 'recorded_artifacts.get("topology_contract") == topology_contract' in script
assert '"topology_contract": topology_contract' in script
def test_owned_image_ancestry_can_fail_closed_to_the_canonical_gitea_store():
fake = FakePortainer()
McpHubDeploymentReconciliation(fake).run()
File diff suppressed because it is too large. Load diff
+443
View File
@@ -0,0 +1,443 @@
from __future__ import annotations
import copy
import importlib.util
import json
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
STORE_PATH = ROOT / "ops" / "gitea-deploy-broker" / "mcp_hub_release_store.py"
def _load_store():
spec = importlib.util.spec_from_file_location("mcp_hub_release_store_test", STORE_PATH)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
@pytest.fixture
def store():
return _load_store()
def _volume(store, **overrides):
value = {
"Name": store.VOLUME_NAME,
"Driver": "local",
"Scope": "local",
"Mountpoint": store.VOLUME_HOST_ROOT.as_posix(),
"Options": None,
"Labels": dict(store.EXPECTED_LABELS),
}
return {**value, **overrides}
def test_fixed_volume_contract_rejects_unexpected_options_labels_and_root(store):
assert store.validate_volume_metadata(_volume(store), "/var/lib/docker") is None
bad_documents = [
_volume(store, Name="other-volume"),
_volume(store, Driver="local-persist"),
_volume(store, Scope="global"),
_volume(store, Mountpoint="/srv/example/appdata/releases"),
_volume(store, Options={"o": "bind", "device": "/tmp"}),
_volume(store, Labels={**store.EXPECTED_LABELS, "attacker": "extra"}),
_volume(store, Labels={"itworx.managed-by": "deployment-controller"}),
]
for document in bad_documents:
with pytest.raises(store.ReleaseStoreError):
store.validate_volume_metadata(document, "/var/lib/docker")
with pytest.raises(store.ReleaseStoreError):
store.validate_volume_metadata(_volume(store), "/var/lib/docker-alt")
def test_release_store_fails_closed_when_acl_metadata_cannot_be_inspected(store, monkeypatch):
def unavailable(_fd):
raise OSError("filesystem ACL metadata is unavailable")
monkeypatch.setattr(store.os, "listxattr", unavailable, raising=False)
with pytest.raises(store.ReleaseStoreError, match="cannot inspect filesystem ACLs"):
store._require_no_acl(1)
@pytest.mark.skipif(
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="Hub state reader verifies root-owned fixed storage",
)
@pytest.mark.parametrize("state_mode", [0o700, 0o755])
def test_fixed_hub_state_reader_is_bounded_and_rejects_symlink_or_missing_state(
store, tmp_path, monkeypatch, state_mode
):
controller_root = tmp_path / "controller"
state_root = controller_root / "state"
controller_root.mkdir(mode=0o700)
controller_root.chmod(0o700)
monkeypatch.setattr(store, "CONTROLLER_STATE_PARENT", controller_root)
monkeypatch.setattr(store, "CONTROLLER_STATE_DIRECTORY", state_root)
assert store.read_hub_state_bytes() is None
with pytest.raises(store.ReleaseStoreError):
store.read_hub_state_bytes(missing_ok=False)
state_root.mkdir(mode=state_mode)
state_root.chmod(state_mode)
state_file = state_root / store.HUB_STATE_FILENAME
state_file.write_bytes(b'{"status":"success"}')
state_file.chmod(0o600)
assert store.read_hub_state_bytes(missing_ok=False) == b'{"status":"success"}'
attacker_root = tmp_path / "attacker"
attacker_root.mkdir(mode=0o700)
attacker_state = attacker_root / store.HUB_STATE_FILENAME
attacker_state.write_bytes(b'{"status":"attacker"}')
attacker_state.chmod(0o600)
state_file.unlink()
state_file.symlink_to(attacker_state)
with pytest.raises(store.ReleaseStoreError, match="unavailable or untrusted"):
store.read_hub_state_bytes()
assert attacker_state.read_bytes() == b'{"status":"attacker"}'
@pytest.mark.skipif(
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="Hub state reader verifies root-owned fixed storage",
)
@pytest.mark.parametrize("state_mode", [0o775, 0o777, 0o750])
def test_fixed_hub_state_reader_rejects_unapproved_directory_modes(
store, tmp_path, monkeypatch, state_mode
):
controller = tmp_path / "controller"
state = controller / "state"
state.mkdir(parents=True)
controller.chmod(0o755)
state.chmod(state_mode)
state_file = state / store.HUB_STATE_FILENAME
state_file.write_bytes(b'{"status":"success"}')
state_file.chmod(0o600)
monkeypatch.setattr(store, "CONTROLLER_STATE_PARENT", controller)
monkeypatch.setattr(store, "CONTROLLER_STATE_DIRECTORY", state)
with pytest.raises(store.ReleaseStoreError, match="unavailable or untrusted"):
store.read_hub_state_bytes()
@pytest.mark.skipif(
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="Hub state reader path-swap test requires root-owned POSIX fixtures",
)
def test_fixed_hub_state_reader_rejects_parent_swap_during_read(store, tmp_path, monkeypatch):
controller_root = tmp_path / "controller"
state_root = controller_root / "state"
state_root.mkdir(parents=True, mode=0o700)
controller_root.chmod(0o700)
state_file = state_root / store.HUB_STATE_FILENAME
state_file.write_bytes(b"trusted")
state_file.chmod(0o600)
attacker_root = tmp_path / "attacker"
attacker_state_root = attacker_root / "state"
attacker_state_root.mkdir(parents=True, mode=0o700)
attacker_file = attacker_state_root / store.HUB_STATE_FILENAME
attacker_file.write_bytes(b"attacker")
attacker_file.chmod(0o600)
monkeypatch.setattr(store, "CONTROLLER_STATE_PARENT", controller_root)
monkeypatch.setattr(store, "CONTROLLER_STATE_DIRECTORY", state_root)
original_read_at = store.read_at
def read_then_swap(*args, **kwargs):
result = original_read_at(*args, **kwargs)
controller_root.rename(tmp_path / "controller.saved")
controller_root.symlink_to(attacker_root, target_is_directory=True)
return result
monkeypatch.setattr(store, "read_at", read_then_swap)
with pytest.raises(store.ReleaseStoreError, match="unavailable or untrusted"):
store.read_hub_state_bytes()
assert attacker_file.read_bytes() == b"attacker"
_RENDERED_2A948_SERVICES = (
"control-api",
"connector-appops",
"connector-gitea",
"connector-ingestion",
"connector-knowledge",
"connector-mobilityops",
"connector-projectbrain",
"connector-stitch",
"connector-unraid",
"contextforge",
"dataplane-router",
"docker-read-broker",
"ingestion-clamd",
"postgres",
"redis",
"web",
"worker",
)
_RENDERED_2A948_RELEASE_BINDS = (
("connector-gitea", "services/runtime/connector_gitea.py", "/opt/hub/services/runtime/connector_gitea.py", "file"),
("contextforge", "infrastructure/images/contextforge/entrypoint.sh", "/opt/itworx/contextforge-entrypoint.sh", "file"),
("contextforge", "infrastructure/images/contextforge/runtime-contract.json", "/opt/itworx/contextforge-runtime-contract.json", "file"),
("contextforge", "scripts/contextforge_runtime_contract.py", "/opt/itworx/contextforge-runtime-contract.py", "file"),
("control-api", "catalog", "/opt/hub/catalog", "directory"),
("dataplane-router", "catalog", "/opt/hub/catalog", "directory"),
("dataplane-router", "services/runtime/dataplane.py", "/opt/hub/services/runtime/dataplane.py", "file"),
("postgres", "infrastructure/postgres/init", "/docker-entrypoint-initdb.d", "directory"),
("postgres", "infrastructure/images/postgres/entrypoint.sh", "/opt/itworx/postgres-entrypoint.sh", "file"),
("redis", "infrastructure/images/redis/entrypoint.sh", "/opt/itworx/redis-entrypoint.sh", "file"),
("redis", "infrastructure/images/redis/healthcheck.sh", "/opt/itworx/redis-healthcheck.sh", "file"),
("web", "services/runtime/web.py", "/opt/hub/services/runtime/web.py", "file"),
("worker", "catalog", "/opt/hub/catalog", "directory"),
("worker", "services/runtime/state_sync.py", "/opt/hub/services/runtime/state_sync.py", "file"),
)
def _rendered_2a948_model(store, revision):
"""Mount projection captured from protected Hub main commit 2a948b17."""
services = {name: {"volumes": []} for name in _RENDERED_2A948_SERVICES}
for service, suffix, target, _kind in _RENDERED_2A948_RELEASE_BINDS:
services[service]["volumes"].append(
{
"type": "bind",
"source": store.logical_release_path(revision) + "/" + suffix,
"target": target,
"read_only": True,
"mode": "ro",
}
)
# These are rendered non-release binds and must remain unchanged.
services["docker-read-broker"]["volumes"].append(
{
"type": "bind",
"source": "/var/run/docker.sock",
"target": "/var/run/docker.sock",
"read_only": True,
}
)
services["postgres"]["volumes"].append(
{"type": "volume", "source": "itworx-mcp-hub-postgres", "target": "/var/lib/postgresql/data"}
)
return {"services": services}
def test_exact_rendered_2a948_four_file_model_translates_all_release_binds_only(store):
revision = "a" * 40
model = _rendered_2a948_model(store, revision)
store.validate_release_mount_inventory(model, revision, source_kind="logical")
translated = store.translate_release_mounts(model, revision)
store.validate_release_mount_inventory(translated, revision, source_kind="host")
assert store.RELEASE_BIND_CONTRACT == _RENDERED_2A948_RELEASE_BINDS
assert len(_RENDERED_2A948_RELEASE_BINDS) == 14
for service, suffix, target, _kind in _RENDERED_2A948_RELEASE_BINDS:
original = next(
mount for mount in model["services"][service]["volumes"]
if mount.get("target") == target
)
observed = next(
mount for mount in translated["services"][service]["volumes"]
if mount.get("target") == target
)
assert original["source"] == f"{store.logical_release_path(revision)}/{suffix}"
assert observed["source"] == f"{store.host_release_path(revision)}/{suffix}"
assert observed["read_only"] is True
assert translated["services"]["docker-read-broker"]["volumes"] == model["services"]["docker-read-broker"]["volumes"]
assert translated["services"]["postgres"]["volumes"][-1] == model["services"]["postgres"]["volumes"][-1]
assert model["services"]["control-api"]["volumes"][0]["source"].startswith(
store.logical_release_path(revision)
)
mutations = []
missing = copy.deepcopy(model)
missing["services"]["redis"]["volumes"].pop(0)
mutations.append(missing)
writable = copy.deepcopy(model)
writable["services"]["worker"]["volumes"][0]["read_only"] = False
mutations.append(writable)
wrong_service = copy.deepcopy(model)
wrong_service["services"]["contextforge"]["volumes"][0]["target"] = "/tmp/entrypoint.sh"
mutations.append(wrong_service)
undeclared_leaf = copy.deepcopy(model)
undeclared_leaf["services"]["control-api"]["volumes"].append(
{
"type": "bind",
"source": f"{store.logical_release_path(revision)}/scripts",
"target": "/opt/hub/scripts",
"read_only": True,
}
)
mutations.append(undeclared_leaf)
release_root = copy.deepcopy(model)
release_root["services"]["control-api"]["volumes"][0]["source"] = store.logical_release_path(revision)
mutations.append(release_root)
volume_ancestor = copy.deepcopy(model)
volume_ancestor["services"]["control-api"]["volumes"][0]["source"] = store.DOCKER_ROOT.as_posix()
mutations.append(volume_ancestor)
for unsafe in mutations:
with pytest.raises(store.ReleaseStoreError):
store.translate_release_mounts(unsafe, revision)
def test_private_overlay_contains_only_fixed_volume_leaf_sources(store):
revision = "b" * 40
content = json.loads(store.compose_override_bytes(revision))
text = json.dumps(content)
assert "${" not in text
assert "secret" not in text.lower()
mounts = [
(service, mount)
for service, config in content["services"].items()
for mount in config["volumes"]
]
assert len(mounts) == len(_RENDERED_2A948_RELEASE_BINDS)
for service, suffix, target, _kind in _RENDERED_2A948_RELEASE_BINDS:
mount = next(item for name, item in mounts if name == service and item["target"] == target)
assert mount["source"] == f"{store.host_release_path(revision)}/{suffix}"
assert mount["type"] == "bind"
assert mount["read_only"] is True
assert mount["bind"] == {"create_host_path": False}
assert "privileged" not in text and "/controller-hub-releases:" not in text
@pytest.mark.skipif(
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
reason="release-leaf identity checks require root-owned POSIX fixtures",
)
def test_release_leaf_verifier_allows_only_regular_files_and_declared_directories(store, tmp_path, monkeypatch):
revision = "c" * 40
volume_root = tmp_path / "volume"
releases = volume_root / store.RELEASES_DIRECTORY
release = releases / revision
release.mkdir(parents=True, mode=0o700)
releases.chmod(0o700)
unique: dict[str, str] = {}
for _service, suffix, _target, kind in store.RELEASE_BIND_CONTRACT:
unique[suffix] = kind
for suffix, kind in unique.items():
target = release.joinpath(*suffix.split("/"))
if kind == "directory":
target.mkdir(parents=True, mode=0o700, exist_ok=True)
else:
target.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
target.write_bytes(b"protected")
target.chmod(0o444)
for path in sorted(release.rglob("*"), reverse=True):
if path.is_dir():
path.chmod(0o555)
release.chmod(0o555)
pinned = store.PinnedDirectory.open(volume_root, check_ancestors=False)
fixed = store.FixedReleaseStore(host=pinned, logical=pinned)
monkeypatch.setattr(fixed, "assert_current", lambda: None)
try:
store.verify_release_bind_leaves(fixed, revision)
bad_file = release / "infrastructure" / "images" / "redis" / "entrypoint.sh"
bad_file.parent.chmod(0o755)
bad_file.unlink()
outside = tmp_path / "outside.sh"
outside.write_text("attacker", encoding="utf-8")
bad_file.symlink_to(outside)
bad_file.parent.chmod(0o555)
with pytest.raises(store.ReleaseStoreError, match="symlink"):
store.verify_release_bind_leaves(fixed, revision)
finally:
pinned.close()
@pytest.mark.skipif(os.name == "nt", reason="descriptor identity checks require POSIX openat")
def test_pinned_store_refuses_ancestor_symlink_swap_before_path_readback(store, tmp_path):
parent = tmp_path / "shared"
parent.mkdir(mode=0o700)
original = parent / "controller"
original.mkdir(mode=0o700)
marker = original / "state.json"
marker.write_bytes(b"old-state")
marker.chmod(0o600)
pinned = store.PinnedDirectory.open(original, check_ancestors=False)
attacker = tmp_path / "attacker"
attacker.mkdir(mode=0o700)
(attacker / "state.json").write_bytes(b"attacker-state")
moved = parent / "controller.saved"
try:
original.rename(moved)
original.symlink_to(attacker, target_is_directory=True)
with pytest.raises(store.ReleaseStoreError, match="path identity changed"):
pinned.assert_current(original)
assert store.read_at(pinned.fd, "state.json", max_bytes=64) == b"old-state"
assert (attacker / "state.json").read_bytes() == b"attacker-state"
finally:
pinned.close()
@pytest.mark.skipif(os.name == "nt", reason="atomic dirfd replacement requires POSIX openat")
def test_pinned_atomic_replace_uses_original_inode_and_detects_swapped_path(store, tmp_path):
parent = tmp_path / "shared"
parent.mkdir(mode=0o700)
original = parent / "controller"
original.mkdir(mode=0o700)
pinned = store.PinnedDirectory.open(original, check_ancestors=False)
attacker = tmp_path / "attacker"
attacker.mkdir(mode=0o700)
def swap_path():
original.rename(parent / "controller.saved")
original.symlink_to(attacker, target_is_directory=True)
try:
with pytest.raises(store.ReleaseStoreError, match="path identity changed"):
store.atomic_replace_at(
pinned.fd,
"state.json",
b"new-state",
expected=None,
after_replace=swap_path,
path_for_identity_check=original,
)
with pytest.raises(FileNotFoundError):
store.read_at(pinned.fd, "state.json", max_bytes=64)
assert not (attacker / "state.json").exists()
finally:
pinned.close()
@pytest.mark.skipif(os.name == "nt", reason="atomic dirfd replacement requires POSIX openat")
def test_pinned_atomic_replace_restores_previous_bytes_after_ancestor_swap(store, tmp_path):
parent = tmp_path / "shared"
parent.mkdir(mode=0o700)
original = parent / "controller"
original.mkdir(mode=0o700)
state = original / "state.json"
state.write_bytes(b"old-state")
state.chmod(0o600)
pinned = store.PinnedDirectory.open(original, check_ancestors=False)
attacker = tmp_path / "attacker"
attacker.mkdir(mode=0o700)
def swap_path():
original.rename(parent / "controller.saved")
original.symlink_to(attacker, target_is_directory=True)
try:
with pytest.raises(store.ReleaseStoreError, match="path identity changed"):
store.atomic_replace_at(
pinned.fd,
"state.json",
b"candidate-state",
expected=b"old-state",
path_for_identity_check=original,
after_replace=swap_path,
)
assert store.read_at(pinned.fd, "state.json", max_bytes=64) == b"old-state"
assert not (attacker / "state.json").exists()
finally:
pinned.close()
@@ -42,6 +42,16 @@ def _sha(value: str) -> str:
return hashlib.sha256(value.encode("ascii")).hexdigest()
def _second_process_lock(path_text, started, locked):
import fcntl
started.set()
with open(path_text, "a+") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
locked.set()
fcntl.flock(handle, fcntl.LOCK_UN)
def test_intent_binding_matches_appops_canonical_digest() -> None:
module = _load_store()
idempotency = _sha("repopulse-adoption-key")
@@ -799,19 +809,10 @@ def test_project_flock_blocks_a_second_process(store_env):
ready = multiprocessing.Event()
acquired = multiprocessing.Event()
def second_process_lock(path_text, started, locked):
import fcntl
started.set()
with open(path_text, "a+") as handle:
fcntl.flock(handle, fcntl.LOCK_EX)
locked.set()
fcntl.flock(handle, fcntl.LOCK_UN)
with store.transaction():
assert store.load_intent() is None
child = multiprocessing.Process(
target=second_process_lock,
target=_second_process_lock,
args=(str(module.PROJECT_LOCK), ready, acquired),
)
child.start()
+600
View File
@@ -202,3 +202,603 @@ def test_operational_users_cannot_change_enrollment_trust(identity_client):
sign_in(identity_client, "owner")
# Administrator reaches schema validation; an operator is rejected first.
assert identity_client.put("/api/enrollments/trust", json={}, headers=headers).status_code == 422
def _enrollment_trust_payload(*, appdata_root: str, backup_acknowledged: bool) -> dict:
return {
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": appdata_root,
"managed_appdata_backup_acknowledged": backup_acknowledged,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
}
@pytest.mark.parametrize(
("initial_root", "initial_ack", "requested_root", "requested_ack"),
[
(
"/srv/example/appdata",
False,
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
True,
),
(
"/var/lib/docker/volumes/itworx-managed-appdata/_data",
True,
"/srv/example/appdata",
False,
),
],
ids=["initial-managed-selection", "managed-root-reset"],
)
def test_settings_manage_put_cannot_change_storage_root(
identity_client, initial_root, initial_ack, requested_root, requested_ack
):
from sqlmodel import Session, select
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
before = _enrollment_trust_payload(
appdata_root=initial_root, backup_acknowledged=initial_ack
)
with Session(database.engine) as db:
db.add(AppSetting(key="deployment_enrollment:trust", value_json=before, is_secret=False))
db.commit()
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
sign_in(identity_client, "reader")
response = identity_client.put(
"/api/enrollments/trust",
json=_enrollment_trust_payload(
appdata_root=requested_root, backup_acknowledged=requested_ack
),
headers=headers,
)
assert response.status_code == 403
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None and saved.value_json == before
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
def test_settings_manage_put_cannot_create_initial_managed_trust(identity_client):
from sqlmodel import Session, select
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
with Session(database.engine) as db:
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
sign_in(identity_client, "reader")
response = identity_client.put(
"/api/enrollments/trust",
json=_enrollment_trust_payload(
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
backup_acknowledged=True,
),
headers=headers,
)
assert response.status_code == 403
with Session(database.engine) as db:
assert db.get(AppSetting, "deployment_enrollment:trust") is None
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
def test_settings_manage_put_can_create_initial_legacy_trust(identity_client):
from sqlmodel import Session
from backend.app.db import session as database
from backend.app.db.models import AppSetting
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
sign_in(identity_client, "reader")
legacy = _enrollment_trust_payload(
appdata_root="/srv/example/appdata", backup_acknowledged=False
)
response = identity_client.put("/api/enrollments/trust", json=legacy, headers=headers)
assert response.status_code == 200
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
assert saved.value_json["managed_appdata_backup_acknowledged"] is False
def test_settings_manage_put_keeps_other_trust_edits_and_admin_can_select_managed_root(identity_client):
from sqlmodel import Session
from backend.app.db import session as database
from backend.app.db.models import AppSetting
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
legacy = _enrollment_trust_payload(
appdata_root="/srv/example/appdata", backup_acknowledged=False
)
with Session(database.engine) as db:
db.add(AppSetting(key="deployment_enrollment:trust", value_json=legacy, is_secret=False))
db.commit()
sign_in(identity_client, "reader")
unrelated_update = {
**legacy,
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
}
response = identity_client.put(
"/api/enrollments/trust", json=unrelated_update, headers=headers
)
assert response.status_code == 200
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
assert saved.value_json["managed_appdata_backup_acknowledged"] is False
sign_in(identity_client, "owner")
managed = _enrollment_trust_payload(
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
backup_acknowledged=True,
)
response = identity_client.put("/api/enrollments/trust", json=managed, headers=headers)
assert response.status_code == 200
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json["trusted_appdata_root"] == managed["trusted_appdata_root"]
assert saved.value_json["managed_appdata_backup_acknowledged"] is True
def test_settings_manage_put_cas_preserves_concurrent_admin_storage_selection(identity_client):
from sqlalchemy import event
from sqlmodel import Session, select
from backend.app.core.time import utc_now
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
from backend.app.services.enrollment_plane import TRUST_SETTING_KEY
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
original = _enrollment_trust_payload(
appdata_root="/srv/example/appdata", backup_acknowledged=False
)
with database.engine.connect() as connection:
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
with Session(database.engine) as db:
db.add(AppSetting(key=TRUST_SETTING_KEY, value_json=original, is_secret=False))
db.commit()
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
sign_in(identity_client, "reader")
injected = False
def select_managed_root_before_put_write(
_connection, _cursor, statement, parameters, _context, _executemany
):
nonlocal injected
normalized = statement.lstrip().upper()
if (
not injected
and normalized.startswith("UPDATE APP_SETTINGS")
and TRUST_SETTING_KEY in str(parameters)
):
injected = True
with Session(database.engine) as concurrent_session:
row = concurrent_session.get(AppSetting, TRUST_SETTING_KEY)
assert row is not None
row.value_json = {
**row.value_json,
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
}
row.updated_at = utc_now()
concurrent_session.add(row)
concurrent_session.commit()
event.listen(database.engine, "before_cursor_execute", select_managed_root_before_put_write)
try:
response = identity_client.put(
"/api/enrollments/trust",
json={
**original,
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
},
headers=headers,
)
finally:
event.remove(database.engine, "before_cursor_execute", select_managed_root_before_put_write)
assert injected is True
assert response.status_code == 409
with Session(database.engine) as db:
saved = db.get(AppSetting, TRUST_SETTING_KEY)
assert saved is not None
assert saved.value_json["trusted_appdata_root"] == "/var/lib/docker/volumes/itworx-managed-appdata/_data"
assert saved.value_json["managed_appdata_backup_acknowledged"] is True
assert saved.value_json["approved_networks"] == original["approved_networks"]
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
def test_settings_manage_initial_legacy_put_loses_if_admin_creates_trust_concurrently(identity_client):
from sqlalchemy import event
from sqlmodel import Session, select
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
from backend.app.services.enrollment_plane import TRUST_SETTING_KEY
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
with database.engine.connect() as connection:
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
with Session(database.engine) as db:
audit_ids_before = set(db.exec(select(AuditEvent.id)).all())
sign_in(identity_client, "reader")
admin_value = _enrollment_trust_payload(
appdata_root="/var/lib/docker/volumes/itworx-managed-appdata/_data",
backup_acknowledged=True,
)
injected = False
def create_admin_trust_before_put_insert(
_connection, _cursor, statement, parameters, _context, _executemany
):
nonlocal injected
normalized = statement.lstrip().upper()
if (
not injected
and normalized.startswith("INSERT INTO APP_SETTINGS")
and TRUST_SETTING_KEY in str(parameters)
):
injected = True
with Session(database.engine) as concurrent_session:
concurrent_session.add(
AppSetting(key=TRUST_SETTING_KEY, value_json=admin_value, is_secret=False)
)
concurrent_session.commit()
event.listen(database.engine, "before_cursor_execute", create_admin_trust_before_put_insert)
try:
response = identity_client.put(
"/api/enrollments/trust",
json=_enrollment_trust_payload(
appdata_root="/srv/example/appdata", backup_acknowledged=False
),
headers=headers,
)
finally:
event.remove(database.engine, "before_cursor_execute", create_admin_trust_before_put_insert)
assert injected is True
assert response.status_code == 409
with Session(database.engine) as db:
saved = db.get(AppSetting, TRUST_SETTING_KEY)
assert saved is not None and saved.value_json == admin_value
assert set(db.exec(select(AuditEvent.id)).all()) == audit_ids_before
def test_storage_root_patch_is_admin_only_and_preserves_other_trust_fields(identity_client):
from sqlmodel import Session, select
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
sign_in(identity_client, "owner")
original = {
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": "/srv/example/appdata",
"managed_appdata_backup_acknowledged": False,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
}
with Session(database.engine) as db:
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
db.commit()
headers = {"X-AppOps-Session-CSRF": "1"}
managed_root = "/var/lib/docker/volumes/itworx-managed-appdata/_data"
updated = identity_client.patch(
"/api/enrollments/trust/storage-root",
json={"trusted_appdata_root": managed_root, "managed_appdata_backup_acknowledged": True},
headers=headers,
)
assert updated.status_code == 200
assert updated.json()["trustedAppdataRoot"] == managed_root
assert updated.json()["managedAppdataBackupAcknowledged"] is True
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json == {
**original,
"trusted_appdata_root": managed_root,
"managed_appdata_backup_acknowledged": True,
}
event = db.exec(
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
).one()
assert event.before_state == {
"trustedAppdataRoot": "/srv/example/appdata",
"managedAppdataBackupAcknowledged": False,
}
assert event.after_state == {
"trustedAppdataRoot": managed_root,
"managedAppdataBackupAcknowledged": True,
}
assert event.redacted_metadata == {
"backupLocation": "dockerRootDir",
"requestSource": "manual",
}
assert "bootstrap_secret_ref" not in str(event.before_state)
assert identity_client.patch(
"/api/enrollments/trust/storage-root",
json={"trusted_appdata_root": managed_root, "managed_appdata_backup_acknowledged": False},
headers=headers,
).status_code == 422
assert identity_client.patch(
"/api/enrollments/trust/storage-root",
json={
"trusted_appdata_root": "/srv/example/other",
"managed_appdata_backup_acknowledged": False,
},
headers=headers,
).status_code == 422
assert identity_client.patch(
"/api/enrollments/trust/storage-root",
json={
"trusted_appdata_root": managed_root,
"managed_appdata_backup_acknowledged": True,
"bootstrap_secret_ref": "LEAKED_SECRET_REFERENCE",
},
headers=headers,
).status_code == 422
legacy = identity_client.patch(
"/api/enrollments/trust/storage-root",
json={"trusted_appdata_root": "/srv/example/appdata", "managed_appdata_backup_acknowledged": False},
headers=headers,
)
assert legacy.status_code == 200
assert legacy.json()["trustedAppdataRoot"] == "/srv/example/appdata"
assert legacy.json()["managedAppdataBackupAcknowledged"] is False
def test_storage_root_patch_returns_conflict_after_concurrent_trust_change(identity_client, monkeypatch):
from sqlmodel import Session, select
from backend.app.core.time import utc_now
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
from backend.app.services.enrollment_plane import EnrollmentPlane
sign_in(identity_client, "owner")
original = {
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": "/srv/example/appdata",
"managed_appdata_backup_acknowledged": False,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
}
with Session(database.engine) as db:
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
db.commit()
stale_trust = EnrollmentTrustConfigV1.model_validate(original)
def read_stale_after_concurrent_update(_session):
with Session(database.engine) as concurrent_session:
row = concurrent_session.get(AppSetting, "deployment_enrollment:trust")
assert row is not None
row.value_json = {**row.value_json, "approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"]}
row.updated_at = utc_now()
concurrent_session.add(row)
concurrent_session.commit()
return stale_trust
monkeypatch.setattr(
EnrollmentPlane,
"get_trust_config",
staticmethod(read_stale_after_concurrent_update),
)
response = identity_client.patch(
"/api/enrollments/trust/storage-root",
json={
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
},
headers={"X-AppOps-Session-CSRF": "1"},
)
assert response.status_code == 409
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
assert db.exec(
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
).all() == []
def test_storage_root_patch_cas_rejects_change_after_snapshot(identity_client, monkeypatch):
from sqlmodel import Session, select
from backend.app.api import routes_enrollments
from backend.app.core.time import utc_now
from backend.app.db import session as database
from backend.app.db.models import AppSetting, AuditEvent
sign_in(identity_client, "owner")
original = {
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"trusted_appdata_root": "/srv/example/appdata",
"managed_appdata_backup_acknowledged": False,
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 42,
"oidc_issuer": None,
"oidc_role_claim": "roles",
"administrator_group": None,
"support_agent_group": None,
"read_only_group": None,
}
with database.engine.connect() as connection:
connection.exec_driver_sql("PRAGMA journal_mode=WAL")
with Session(database.engine) as db:
db.add(AppSetting(key="deployment_enrollment:trust", value_json=original, is_secret=False))
db.commit()
actual_sql_update = routes_enrollments.sql_update
concurrent_write_done = False
def write_concurrently_after_snapshot(model):
nonlocal concurrent_write_done
if model is AppSetting and not concurrent_write_done:
concurrent_write_done = True
with Session(database.engine) as concurrent_session:
row = concurrent_session.get(AppSetting, "deployment_enrollment:trust")
assert row is not None
row.value_json = {
**row.value_json,
"approved_networks": ["itworx-mcp-hub-npm", "itworx-npm"],
}
row.updated_at = utc_now()
concurrent_session.add(row)
concurrent_session.commit()
return actual_sql_update(model)
# The endpoint has already read and validated its selector snapshot when it
# calls sql_update. Commit an independent trust edit at that boundary so the
# route's compare-and-swap must reject the now-stale write.
monkeypatch.setattr(routes_enrollments, "sql_update", write_concurrently_after_snapshot)
response = identity_client.patch(
"/api/enrollments/trust/storage-root",
json={
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
},
headers={"X-AppOps-Session-CSRF": "1"},
)
assert concurrent_write_done is True
assert response.status_code == 409
with Session(database.engine) as db:
saved = db.get(AppSetting, "deployment_enrollment:trust")
assert saved is not None
assert saved.value_json["approved_networks"] == ["itworx-mcp-hub-npm", "itworx-npm"]
assert saved.value_json["trusted_appdata_root"] == "/srv/example/appdata"
assert db.exec(
select(AuditEvent).where(AuditEvent.action_id == "enrollment.trust.storage_root.update")
).all() == []
def test_read_only_and_operator_cannot_change_storage_root(identity_client):
reader_id = sign_in(identity_client, "reader")
sign_in(identity_client, "owner")
headers = {"X-AppOps-Session-CSRF": "1"}
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "operator", "enabled": True, "permissions": []},
headers=headers,
).status_code == 200
selection = {
"trusted_appdata_root": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
"managed_appdata_backup_acknowledged": True,
}
sign_in(identity_client, "reader")
assert identity_client.patch(
"/api/enrollments/trust/storage-root", json=selection, headers=headers
).status_code == 403
sign_in(identity_client, "owner")
assert identity_client.patch(
f"/api/auth/users/{reader_id}",
json={"role": "custom", "enabled": True, "permissions": ["settings.manage"]},
headers=headers,
).status_code == 200
sign_in(identity_client, "reader")
assert identity_client.patch(
"/api/enrollments/trust/storage-root", json=selection, headers=headers
).status_code == 403
sign_in(identity_client, "read-only")
assert identity_client.patch(
"/api/enrollments/trust/storage-root", json=selection, headers=headers
).status_code == 403