Publish curated Unraid-AppOps-Gateway source from 451646fee806a47e3256731766980a05a262dcdd
This commit is contained in:
1 parent
532f3d9f26
commit
0a82153d95
43 files changed
+17722
-93
No files matched your search
@@ -28,6 +28,7 @@ REVISION = "a" * 40
|
||||
TREE = "b" * 40
|
||||
CALLER_ID = "c" * 64
|
||||
MODERN_HELPER_ID = "f" * 64
|
||||
RETIRED_HELPER_ID = bootstrap.RETIRED_FAILED_MODERN_HELPER_ID
|
||||
HOST_HOSTNAME = "Tower"
|
||||
|
||||
|
||||
@@ -99,7 +100,7 @@ bash "$work/upgrade.sh" \\
|
||||
'''
|
||||
|
||||
|
||||
def _legacy_helper_spec() -> dict[str, Any]:
|
||||
def _legacy_helper_spec(revision: str = REVISION) -> dict[str, Any]:
|
||||
tree = ast.parse(LEGACY_HELPER_SOURCE_FIXTURE)
|
||||
function = next(
|
||||
node
|
||||
@@ -121,7 +122,7 @@ def _legacy_helper_spec() -> dict[str, Any]:
|
||||
compile(ast.Module(body=[function], type_ignores=[]), "legacy-promoter", "exec"),
|
||||
namespace,
|
||||
)
|
||||
return namespace["helper_container_spec"](revision=REVISION, image_id=CONTROLLER_IMAGE_ID)
|
||||
return namespace["helper_container_spec"](revision=revision, image_id=CONTROLLER_IMAGE_ID)
|
||||
|
||||
|
||||
def _image(labels: dict[str, str]) -> dict[str, Any]:
|
||||
@@ -409,14 +410,43 @@ class FakeDocker:
|
||||
self.existing_child = existing_child
|
||||
self.created: list[dict[str, Any]] = []
|
||||
self.started: list[str] = []
|
||||
self.removed: list[str] = []
|
||||
self.inspected: list[str] = []
|
||||
self.host_name = HOST_HOSTNAME
|
||||
self.log_bytes = b"controller_v2_ready revision=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n"
|
||||
self.named_inspect_override: dict[str, Any] | None = None
|
||||
self.named_inspect_override_at = 0
|
||||
self.retired_id_inspect_override: dict[str, Any] | None = None
|
||||
self.appops_inspect_count = 0
|
||||
self.appops_inspect_override: dict[str, Any] | None = None
|
||||
self.appops_inspect_override_at = 0
|
||||
self._inspect_counts: dict[str, int] = {}
|
||||
|
||||
def inspect_container(self, reference: str) -> dict[str, Any] | None:
|
||||
self.inspected.append(reference)
|
||||
self._inspect_counts[reference] = self._inspect_counts.get(reference, 0) + 1
|
||||
if reference == "itworx-controller-artifact-promoter-modern":
|
||||
if (
|
||||
self.named_inspect_override is not None
|
||||
and self._inspect_counts[reference] == self.named_inspect_override_at
|
||||
):
|
||||
return self.named_inspect_override
|
||||
return self.existing_child
|
||||
if (
|
||||
reference == RETIRED_HELPER_ID
|
||||
and self.retired_id_inspect_override is not None
|
||||
):
|
||||
return self.retired_id_inspect_override
|
||||
if (
|
||||
reference == RETIRED_HELPER_ID
|
||||
and self.existing_child is not None
|
||||
and self.existing_child.get("Id") == RETIRED_HELPER_ID
|
||||
):
|
||||
return self.existing_child
|
||||
if reference == APPOPS_CONTAINER_ID:
|
||||
self.appops_inspect_count += 1
|
||||
if self.appops_inspect_count == self.appops_inspect_override_at:
|
||||
return self.appops_inspect_override
|
||||
return self.containers.get(reference)
|
||||
|
||||
def daemon_name(self) -> str:
|
||||
@@ -444,8 +474,15 @@ class FakeDocker:
|
||||
source_revision = self.created[-1]["Labels"]["itworx.source.revision"]
|
||||
self.containers["gitea-deploy-control"] = _modern_controller_inspect(source_revision)
|
||||
|
||||
def remove_retired_failed_container(self, identifier: str) -> None:
|
||||
assert identifier == RETIRED_HELPER_ID
|
||||
assert self.existing_child is not None
|
||||
assert self.existing_child["Id"] == RETIRED_HELPER_ID
|
||||
self.removed.append(identifier)
|
||||
self.existing_child = None
|
||||
|
||||
def container_logs(self, identifier: str, *, tail: int, max_bytes: int) -> bytes:
|
||||
assert identifier == MODERN_HELPER_ID
|
||||
assert identifier in {MODERN_HELPER_ID, RETIRED_HELPER_ID}
|
||||
assert tail <= 40
|
||||
assert max_bytes <= 8192
|
||||
return self.log_bytes[:max_bytes]
|
||||
@@ -458,9 +495,13 @@ class FakeProtectedGit:
|
||||
self.helper_source = helper_source
|
||||
self.blobs_read: list[tuple[str, str]] = []
|
||||
self.main_checks = 0
|
||||
self.fail_current_main_at: int | None = None
|
||||
self.retired_tree_valid = True
|
||||
|
||||
def verify_current_main(self, revision: str, tree: str) -> None:
|
||||
self.main_checks += 1
|
||||
if self.fail_current_main_at == self.main_checks:
|
||||
raise bootstrap.BootstrapError("controller_promoter_source_not_current_main")
|
||||
if (revision, tree) != (self.revision, self.tree):
|
||||
raise bootstrap.BootstrapError("controller_promoter_source_not_current_main")
|
||||
|
||||
@@ -468,9 +509,18 @@ class FakeProtectedGit:
|
||||
if (revision, tree) != (LEGACY_REVISION, LEGACY_TREE):
|
||||
raise bootstrap.BootstrapError("controller_promoter_legacy_provenance_invalid")
|
||||
|
||||
def verify_retired_modern_revision(self, revision: str, tree: str) -> None:
|
||||
if (revision, tree) != (
|
||||
bootstrap.RETIRED_FAILED_MODERN_REVISION,
|
||||
bootstrap.RETIRED_FAILED_MODERN_TREE,
|
||||
) or not self.retired_tree_valid:
|
||||
raise bootstrap.BootstrapError(
|
||||
"controller_promoter_retired_helper_provenance_invalid"
|
||||
)
|
||||
|
||||
def read_blob(self, revision: str, path: str) -> bytes:
|
||||
self.blobs_read.append((revision, path))
|
||||
if revision != self.revision or path != "backend/app/services/controller_artifact_helper_spec.py":
|
||||
if revision not in {self.revision, bootstrap.RETIRED_FAILED_MODERN_REVISION} or path != "backend/app/services/controller_artifact_helper_spec.py":
|
||||
raise bootstrap.BootstrapError("controller_promoter_source_blob_invalid")
|
||||
return self.helper_source.encode("utf-8")
|
||||
|
||||
@@ -518,7 +568,10 @@ def _run_bootstrap(
|
||||
*,
|
||||
health: dict[str, Any] | None = None,
|
||||
initial_health: dict[str, Any] | None = None,
|
||||
health_sequence: list[dict[str, Any]] | None = None,
|
||||
hostname: str = HOST_HOSTNAME,
|
||||
source_revision: str = REVISION,
|
||||
source_tree: str = TREE,
|
||||
):
|
||||
source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
@@ -530,15 +583,15 @@ def _run_bootstrap(
|
||||
version="2.1.12",
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
health_reads = [initial, health or _health()]
|
||||
health_reads = health_sequence or [initial, health or _health()]
|
||||
|
||||
def read_health() -> dict[str, Any]:
|
||||
return health_reads.pop(0) if health_reads else health or _health()
|
||||
|
||||
return bootstrap.run_bootstrap(
|
||||
"jens/unraid-appops-gateway",
|
||||
REVISION,
|
||||
TREE,
|
||||
source_revision,
|
||||
source_tree,
|
||||
docker=docker,
|
||||
protected_git=git,
|
||||
hostname=hostname,
|
||||
@@ -549,6 +602,54 @@ def _run_bootstrap(
|
||||
)
|
||||
|
||||
|
||||
def _retired_failed_child(helper_source: str) -> dict[str, Any]:
|
||||
spec = bootstrap.render_modern_helper_spec(
|
||||
helper_source,
|
||||
revision=bootstrap.RETIRED_FAILED_MODERN_REVISION,
|
||||
image_id=CONTROLLER_IMAGE_ID,
|
||||
current_controller_id=CONTROLLER_CONTAINER_ID,
|
||||
current_controller_revision=CONTROLLER_REVISION,
|
||||
current_controller_tree=CONTROLLER_TREE,
|
||||
current_controller_version="2.1.12",
|
||||
)
|
||||
child = _inspect_from_spec(
|
||||
"itworx-controller-artifact-promoter-modern",
|
||||
RETIRED_HELPER_ID,
|
||||
CONTROLLER_IMAGE_ID,
|
||||
spec,
|
||||
state={"Running": False, "ExitCode": 1, "Status": "exited"},
|
||||
)
|
||||
child["Config"]["Volumes"] = None
|
||||
child["Mounts"] = [
|
||||
{"Type": "bind", "Source": "/mnt/cache", "Destination": "/mnt/cache", "RW": True, "Propagation": "rprivate"},
|
||||
{"Type": "bind", "Source": "/mnt/user", "Destination": "/mnt/user", "RW": True, "Propagation": "rprivate"},
|
||||
{"Type": "bind", "Source": "/var/lib/docker", "Destination": "/var/lib/docker", "RW": False, "Propagation": "rslave"},
|
||||
{"Type": "bind", "Source": "/srv/example/appdata/gitea/git/repositories", "Destination": "/gitea-repositories", "RW": False, "Propagation": "rprivate"},
|
||||
{"Type": "bind", "Source": "/var/run/docker.sock", "Destination": "/var/run/docker.sock", "RW": True, "Propagation": "rprivate"},
|
||||
]
|
||||
child["HostConfig"].update(
|
||||
{
|
||||
"Mounts": None,
|
||||
"CapAdd": None,
|
||||
"CapDrop": None,
|
||||
"Devices": None,
|
||||
"DeviceRequests": None,
|
||||
"PortBindings": None,
|
||||
"VolumesFrom": None,
|
||||
"PidMode": "",
|
||||
"UTSMode": "",
|
||||
"UsernsMode": "",
|
||||
"CgroupParent": "",
|
||||
"IpcMode": "private",
|
||||
"CgroupnsMode": "private",
|
||||
"Runtime": "runc",
|
||||
"AutoRemove": False,
|
||||
"PublishAllPorts": False,
|
||||
}
|
||||
)
|
||||
return child
|
||||
|
||||
|
||||
def test_initial_controller_runtime_accepts_exact_current_compose_container():
|
||||
docker = FakeDocker(_legacy_helper_spec())
|
||||
base_image_labels = bootstrap._inspect_controller_base_image(docker)
|
||||
@@ -876,6 +977,187 @@ def test_running_modern_child_returns_unknown_without_duplicate_or_stop():
|
||||
assert docker.started == []
|
||||
|
||||
|
||||
def test_exact_failed_historical_child_is_removed_before_current_helper_launch():
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
legacy_health = _health(
|
||||
revision=CONTROLLER_REVISION,
|
||||
tree=CONTROLLER_TREE,
|
||||
version="2.1.12",
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
|
||||
result = _run_bootstrap(
|
||||
docker,
|
||||
git,
|
||||
health_sequence=[legacy_health, legacy_health, legacy_health, _health()],
|
||||
)
|
||||
|
||||
assert result["status"] == "promoted"
|
||||
assert docker.removed == [RETIRED_HELPER_ID]
|
||||
assert docker.created and docker.started == [MODERN_HELPER_ID]
|
||||
assert RETIRED_HELPER_ID in docker.inspected
|
||||
assert docker.appops_inspect_count == 2
|
||||
assert git.blobs_read.count(
|
||||
(bootstrap.RETIRED_FAILED_MODERN_REVISION, bootstrap.MODERN_HELPER_SOURCE_PATH)
|
||||
) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation",
|
||||
[
|
||||
"running",
|
||||
"successful-exit",
|
||||
"boolean-exit",
|
||||
"other-exit",
|
||||
"wrong-id",
|
||||
"config-volumes",
|
||||
"extra-host-mount",
|
||||
"cap-add",
|
||||
"device-request",
|
||||
"docker-root-writable",
|
||||
"socket-bind-changed",
|
||||
"legacy-controller-stopped",
|
||||
"current-main-changed",
|
||||
"second-name-inspect-changed",
|
||||
"second-id-inspect-changed",
|
||||
"appops-stopped-before-delete",
|
||||
"historical-tree-proof-failed",
|
||||
],
|
||||
)
|
||||
def test_retired_helper_cleanup_fails_closed_on_any_identity_or_state_drift(mutation: str):
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
failed_child = _retired_failed_child(helper_source)
|
||||
docker = FakeDocker(_legacy_helper_spec(), existing_child=failed_child)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
if mutation == "running":
|
||||
failed_child["State"].update({"Running": True, "Status": "running"})
|
||||
elif mutation == "successful-exit":
|
||||
failed_child["State"]["ExitCode"] = 0
|
||||
elif mutation == "boolean-exit":
|
||||
failed_child["State"]["ExitCode"] = True
|
||||
elif mutation == "other-exit":
|
||||
failed_child["State"]["ExitCode"] = 2
|
||||
elif mutation == "wrong-id":
|
||||
failed_child["Id"] = "9" * 64
|
||||
elif mutation == "config-volumes":
|
||||
failed_child["Config"]["Volumes"] = {"/extra": {}}
|
||||
elif mutation == "extra-host-mount":
|
||||
failed_child["HostConfig"]["Mounts"] = [{"Type": "volume"}]
|
||||
elif mutation == "cap-add":
|
||||
failed_child["HostConfig"]["CapAdd"] = ["SYS_ADMIN"]
|
||||
elif mutation == "device-request":
|
||||
failed_child["HostConfig"]["DeviceRequests"] = [{"Capabilities": [["gpu"]]}]
|
||||
elif mutation == "docker-root-writable":
|
||||
failed_child["HostConfig"]["Binds"][2] = "/var/lib/docker:/var/lib/docker:rw"
|
||||
failed_child["Mounts"][2]["RW"] = True
|
||||
elif mutation == "socket-bind-changed":
|
||||
failed_child["HostConfig"]["Binds"][4] = "/tmp/docker.sock:/var/run/docker.sock"
|
||||
failed_child["Mounts"][4]["Source"] = "/tmp/docker.sock"
|
||||
elif mutation == "legacy-controller-stopped":
|
||||
docker.containers["gitea-deploy-control"]["State"]["Running"] = False
|
||||
elif mutation == "current-main-changed":
|
||||
git.fail_current_main_at = 2
|
||||
elif mutation == "second-name-inspect-changed":
|
||||
docker.named_inspect_override = json.loads(json.dumps(failed_child))
|
||||
docker.named_inspect_override["Config"]["Cmd"].append("unexpected")
|
||||
docker.named_inspect_override_at = 2
|
||||
elif mutation == "second-id-inspect-changed":
|
||||
docker.retired_id_inspect_override = json.loads(json.dumps(failed_child))
|
||||
docker.retired_id_inspect_override["State"]["ExitCode"] = 0
|
||||
elif mutation == "appops-stopped-before-delete":
|
||||
docker.appops_inspect_override = json.loads(
|
||||
json.dumps(docker.containers[APPOPS_CONTAINER_ID])
|
||||
)
|
||||
docker.appops_inspect_override["State"]["Running"] = False
|
||||
docker.appops_inspect_override_at = 2
|
||||
elif mutation == "historical-tree-proof-failed":
|
||||
git.retired_tree_valid = False
|
||||
|
||||
legacy_health = _health(
|
||||
revision=CONTROLLER_REVISION,
|
||||
tree=CONTROLLER_TREE,
|
||||
version="2.1.12",
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
with pytest.raises(bootstrap.BootstrapError):
|
||||
_run_bootstrap(
|
||||
docker,
|
||||
git,
|
||||
health_sequence=[legacy_health, legacy_health, legacy_health],
|
||||
)
|
||||
|
||||
assert docker.removed == []
|
||||
assert docker.created == []
|
||||
assert docker.started == []
|
||||
|
||||
|
||||
def test_retired_helper_remove_uses_non_force_non_volume_docker_delete():
|
||||
client = bootstrap.DockerSocketClient()
|
||||
calls: list[tuple[str, str]] = []
|
||||
|
||||
def request(method: str, path: str, **_kwargs: Any) -> tuple[int, bytes]:
|
||||
calls.append((method, path))
|
||||
return 204, b""
|
||||
|
||||
client._request = request # type: ignore[method-assign]
|
||||
client.remove_retired_failed_container(RETIRED_HELPER_ID)
|
||||
|
||||
assert calls == [
|
||||
("DELETE", f"/containers/{RETIRED_HELPER_ID}?force=false&v=false")
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", [404, 409])
|
||||
def test_retired_helper_delete_refuses_unexpected_docker_status(status: int):
|
||||
client = bootstrap.DockerSocketClient()
|
||||
|
||||
def request(_method: str, _path: str, **_kwargs: Any) -> tuple[int, bytes]:
|
||||
return status, b""
|
||||
|
||||
client._request = request # type: ignore[method-assign]
|
||||
with pytest.raises(bootstrap.BootstrapError):
|
||||
client.remove_retired_failed_container(RETIRED_HELPER_ID)
|
||||
|
||||
|
||||
def test_same_failed_source_revision_is_never_retried_or_removed():
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
docker = FakeDocker(
|
||||
_legacy_helper_spec(bootstrap.RETIRED_FAILED_MODERN_REVISION),
|
||||
existing_child=_retired_failed_child(helper_source),
|
||||
)
|
||||
git = FakeProtectedGit(helper_source)
|
||||
git.revision = bootstrap.RETIRED_FAILED_MODERN_REVISION
|
||||
git.tree = bootstrap.RETIRED_FAILED_MODERN_TREE
|
||||
legacy_health = _health(
|
||||
revision=CONTROLLER_REVISION,
|
||||
tree=CONTROLLER_TREE,
|
||||
version="2.1.12",
|
||||
artifact_sha256=bootstrap.LEGACY_CONTROLLER_ARTIFACT_SHA256,
|
||||
)
|
||||
|
||||
result = _run_bootstrap(
|
||||
docker,
|
||||
git,
|
||||
health_sequence=[legacy_health],
|
||||
source_revision=git.revision,
|
||||
source_tree=git.tree,
|
||||
)
|
||||
|
||||
assert result["status"] == "failed"
|
||||
assert result["exitCode"] == 1
|
||||
assert docker.removed == []
|
||||
assert docker.created == []
|
||||
|
||||
|
||||
def test_completed_modern_child_with_wrong_artifact_is_not_relaunched():
|
||||
helper_source = (ROOT / "backend/app/services/controller_artifact_helper_spec.py").read_text(
|
||||
encoding="utf-8"
|
||||
|
||||
@@ -23,6 +23,18 @@ 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"
|
||||
HISTORICAL_CONFIG_ROOT_RELEASE_FILES = (
|
||||
"controller_paths.py", "mcp_hub_release_store.py", "hub_permission_preparation.py",
|
||||
"deploy.py", "managed_appdata_paths.py", "prepare_managed_appdata_root.py", "runner_fleet.py",
|
||||
"runner_capacity_controller.py", "runner_capacity_policy.py", "runner_fleet_upgrade_v2.py",
|
||||
"runner_profiles.py", "runner_admission_policy.py", "runner_admission_controller.py",
|
||||
"runner_resource_envelope.py", "runner_fleet_specialize.py", "runner_statistics_binding.py",
|
||||
"status_server.py", "repopulse_adoption_store.py", "repopulse_registry_restore.py",
|
||||
"repopulse_adoption_executor.py", "repopulse_adoption_effects.py",
|
||||
"repopulse_adoption_storage_effects.py", "repopulse_target_attestation.py",
|
||||
"stage_repopulse_target_attestation.py", "repopulse_adoption_cli.py",
|
||||
"stage_repopulse_maintenance_environment.py", "run_repopulse_adoption.py",
|
||||
)
|
||||
|
||||
|
||||
def volume():
|
||||
@@ -58,6 +70,214 @@ def test_only_the_exact_local_volume_is_accepted():
|
||||
module.validate_volume(volume())
|
||||
|
||||
|
||||
def backup_volume():
|
||||
return {
|
||||
"Name": "itworx-forge-master-key-backup",
|
||||
"Driver": "local",
|
||||
"Scope": "local",
|
||||
"Options": None,
|
||||
"Labels": {
|
||||
"itworx.managed-by": "deployment-controller",
|
||||
"itworx.purpose": "forge-master-key-backup",
|
||||
},
|
||||
"Mountpoint": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
||||
}
|
||||
|
||||
|
||||
def test_backup_volume_identity_is_exact_and_never_repairs_existing_metadata():
|
||||
module.validate_backup_volume(backup_volume())
|
||||
bad = backup_volume()
|
||||
bad["Labels"]["itworx.purpose"] = "foreign"
|
||||
with pytest.raises(module.VolumeTransitionError):
|
||||
module.validate_backup_volume(bad)
|
||||
|
||||
|
||||
def test_promotion_preparer_creates_only_the_fixed_backup_volume_if_absent(monkeypatch, tmp_path):
|
||||
commands = []
|
||||
present = False
|
||||
|
||||
def run(arguments, **_kwargs):
|
||||
nonlocal present
|
||||
commands.append(arguments)
|
||||
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
|
||||
return b'"/var/lib/docker"'
|
||||
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
|
||||
return b"" if not present else b"itworx-forge-master-key-backup\n"
|
||||
if arguments[:3] == ["docker", "volume", "create"]:
|
||||
present = True
|
||||
return b"itworx-forge-master-key-backup\n"
|
||||
if arguments == ["docker", "volume", "inspect", "itworx-forge-master-key-backup"]:
|
||||
return json.dumps([backup_volume()]).encode()
|
||||
raise AssertionError(arguments)
|
||||
|
||||
directory_calls = []
|
||||
monkeypatch.setattr(module, "_run", run)
|
||||
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
|
||||
|
||||
def directory(path, anchor):
|
||||
directory_calls.append((path, anchor))
|
||||
return 23
|
||||
|
||||
monkeypatch.setattr(module, "_directory", directory)
|
||||
module.ensure_backup_volume(create=True)
|
||||
|
||||
create = [arguments for arguments in commands if arguments[:3] == ["docker", "volume", "create"]]
|
||||
assert create == [[
|
||||
"docker", "volume", "create", "--driver", "local",
|
||||
"--label", "itworx.managed-by=deployment-controller",
|
||||
"--label", "itworx.purpose=forge-master-key-backup",
|
||||
"itworx-forge-master-key-backup",
|
||||
]]
|
||||
assert directory_calls == [
|
||||
(module.DOCKER_ROOT / "volumes", module.DOCKER_ROOT),
|
||||
(module.BACKUP_MOUNTPOINT, module.DOCKER_ROOT),
|
||||
]
|
||||
|
||||
|
||||
def test_existing_backup_volume_is_validated_without_relabel_or_recreation(monkeypatch, tmp_path):
|
||||
commands = []
|
||||
|
||||
def run(arguments, **_kwargs):
|
||||
commands.append(arguments)
|
||||
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
|
||||
return b'"/var/lib/docker"'
|
||||
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
|
||||
return b"itworx-forge-master-key-backup\n"
|
||||
if arguments == ["docker", "volume", "inspect", "itworx-forge-master-key-backup"]:
|
||||
item = backup_volume()
|
||||
item["CreatedAt"] = "2000-01-01T00:00:00Z"
|
||||
return json.dumps([item]).encode()
|
||||
raise AssertionError(arguments)
|
||||
|
||||
monkeypatch.setattr(module, "_run", run)
|
||||
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
|
||||
monkeypatch.setattr(
|
||||
module, "_directory",
|
||||
lambda _path, _anchor: 23,
|
||||
)
|
||||
module.ensure_backup_volume(create=False)
|
||||
assert not any(arguments[:3] == ["docker", "volume", "create"] for arguments in commands)
|
||||
|
||||
|
||||
def test_missing_backup_volume_is_not_created_outside_promotion(monkeypatch, tmp_path):
|
||||
commands = []
|
||||
|
||||
def run(arguments, **_kwargs):
|
||||
commands.append(arguments)
|
||||
if arguments == ["docker", "info", "--format", "{{json .DockerRootDir}}"]:
|
||||
return b'"/var/lib/docker"'
|
||||
if arguments == ["docker", "volume", "ls", "--format", "{{.Name}}"]:
|
||||
return b""
|
||||
raise AssertionError(arguments)
|
||||
|
||||
monkeypatch.setattr(module, "_run", run)
|
||||
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
|
||||
monkeypatch.setattr(
|
||||
module, "_directory",
|
||||
lambda _path, _anchor: 23,
|
||||
)
|
||||
with pytest.raises(module.VolumeTransitionError, match="backup_volume_missing"):
|
||||
module.ensure_backup_volume(create=False)
|
||||
assert not any(arguments[:3] == ["docker", "volume", "create"] for arguments in commands)
|
||||
|
||||
|
||||
def _prepare_promotion_doubles(monkeypatch, tmp_path, *, config_valid=True):
|
||||
events = []
|
||||
controller_id = "c" * 64
|
||||
current = {
|
||||
"Id": controller_id,
|
||||
"Mounts": [{
|
||||
"Destination": module.EXECUTABLE_TARGET,
|
||||
"Source": module.CONTROLLER_DIR.as_posix(),
|
||||
}],
|
||||
}
|
||||
previous_identity = {
|
||||
"sourceRevision": "b" * 40,
|
||||
"sourceTree": "c" * 40,
|
||||
"controllerVersion": "2.1.0",
|
||||
}
|
||||
config_tree = ({}, {"": 0o755})
|
||||
monkeypatch.setattr(module, "_validate_private_stage", lambda _stage: events.append("stage_validated"))
|
||||
monkeypatch.setattr(module, "ensure_volume", lambda **_kwargs: None)
|
||||
monkeypatch.setattr(
|
||||
module, "_controller_image_inspect",
|
||||
lambda _image_id: (events.append("controller_inspected") or current),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module, "_snapshot_attested_controller",
|
||||
lambda *_args, **_kwargs: ({}, {"": 0o755}, previous_identity),
|
||||
)
|
||||
monkeypatch.setattr(module, "_validate_previous_private_tls", lambda _stage: None)
|
||||
monkeypatch.setattr(module, "_release_tree", lambda *_args, **_kwargs: ({}, {"": 0o755}))
|
||||
monkeypatch.setattr(module, "_config_tree", lambda *_args, **_kwargs: config_tree)
|
||||
monkeypatch.setattr(module, "_read_config_leaf", lambda: config_tree)
|
||||
monkeypatch.setattr(module, "_prepare_merged_tree", lambda *_args, **_kwargs: ({}, {"": 0o755}))
|
||||
|
||||
def validate_staged(*_args, **_kwargs):
|
||||
events.append("config_validated")
|
||||
if not config_valid:
|
||||
raise module.VolumeTransitionError("staged_config_invalid")
|
||||
|
||||
monkeypatch.setattr(module, "_validate_staged_configuration", validate_staged)
|
||||
monkeypatch.setattr(module, "_install_release_leaf", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(module, "_read_root_compose", lambda: (ORIGINAL_COMPOSE, 0o644))
|
||||
monkeypatch.setattr(module, "_mirror_snapshot_tar", lambda: b"mirror")
|
||||
monkeypatch.setattr(
|
||||
module, "rendered_model",
|
||||
lambda *_args, **_kwargs: (events.append("compose_rendered") or b"rendered-compose"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module, "_verify_compose_image",
|
||||
lambda *_args, **_kwargs: events.append("compose_verified"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
module, "ensure_backup_volume",
|
||||
lambda **_kwargs: events.append("backup_volume_prepared"),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(module, "_release_tar_bytes", lambda *_args, **_kwargs: b"tar")
|
||||
backup = tmp_path / "promotion-backup"
|
||||
backup.mkdir(mode=0o700)
|
||||
backup_fd = 23
|
||||
monkeypatch.setattr(module, "_create_backup", lambda: (backup, backup_fd))
|
||||
monkeypatch.setattr(module, "_write_backup", lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(module.os, "close", lambda _descriptor: None)
|
||||
return events
|
||||
|
||||
|
||||
def test_promotion_prepares_backup_volume_after_source_stage_controller_and_config_checks(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
|
||||
module.prepare_promotion(
|
||||
tmp_path / "stage",
|
||||
source_revision="d" * 40,
|
||||
source_tree="e" * 40,
|
||||
controller_version="2.2.0",
|
||||
image_id=IMAGE_ID,
|
||||
)
|
||||
assert events.index("stage_validated") < events.index("config_validated")
|
||||
assert events.index("config_validated") < events.index("backup_volume_prepared")
|
||||
assert events.index("compose_rendered") < events.index("backup_volume_prepared")
|
||||
assert events.count("compose_verified") == 2
|
||||
assert events.index("compose_verified") < events.index("backup_volume_prepared")
|
||||
|
||||
|
||||
def test_promotion_does_not_prepare_backup_volume_when_staged_configuration_fails(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
events = _prepare_promotion_doubles(monkeypatch, tmp_path, config_valid=False)
|
||||
with pytest.raises(module.VolumeTransitionError, match="staged_config_invalid"):
|
||||
module.prepare_promotion(
|
||||
tmp_path / "stage",
|
||||
source_revision="d" * 40,
|
||||
source_tree="e" * 40,
|
||||
controller_version="2.2.0",
|
||||
image_id=IMAGE_ID,
|
||||
)
|
||||
assert "backup_volume_prepared" not in events
|
||||
|
||||
|
||||
def writer_inventory(inherited_labels):
|
||||
identifier = "c" * 64
|
||||
spec = module._writer_spec(IMAGE_ID, REVISION, kind="config")
|
||||
@@ -230,18 +450,28 @@ def test_compose_uses_protected_code_and_two_config_views(compose):
|
||||
"target": module.TARGET,
|
||||
"volume": {"nocopy": True},
|
||||
} in volumes
|
||||
assert {
|
||||
"type": "volume",
|
||||
"source": "itworx-forge-master-key-backup",
|
||||
"target": "/forge-master-key-backup",
|
||||
"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
|
||||
assert len(volumes) == 5
|
||||
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"] == "/forge-master-key-backup"))
|
||||
volumes.remove(next(item for item in volumes if item["target"] == module.CONFIG_TARGET))
|
||||
assert rendered["volumes"] == {
|
||||
module.VOLUME: {"name": module.VOLUME, "external": True}
|
||||
module.VOLUME: {"name": module.VOLUME, "external": True},
|
||||
"itworx-forge-master-key-backup": {
|
||||
"name": "itworx-forge-master-key-backup", "external": True
|
||||
},
|
||||
}
|
||||
assert rendered["services"]["runner"] == baseline["services"]["runner"]
|
||||
assert service["environment"] == baseline["services"]["deploy-control"]["environment"]
|
||||
@@ -257,6 +487,46 @@ def test_compose_uses_protected_code_and_two_config_views(compose):
|
||||
assert calls[0][1] == ORIGINAL_COMPOSE
|
||||
|
||||
|
||||
def test_compose_allows_only_the_fixed_managed_appdata_volume_mount(compose):
|
||||
baseline, _calls = compose
|
||||
mount = {
|
||||
"type": "volume",
|
||||
"source": module.MANAGED_APPDATA_VOLUME,
|
||||
"target": module.MANAGED_APPDATA_MOUNTPOINT.as_posix(),
|
||||
}
|
||||
baseline["services"][module.SERVICE]["volumes"].append(mount)
|
||||
baseline["volumes"] = {
|
||||
module.MANAGED_APPDATA_VOLUME: {
|
||||
"name": module.MANAGED_APPDATA_VOLUME,
|
||||
"external": True,
|
||||
}
|
||||
}
|
||||
|
||||
rendered = json.loads(module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID))
|
||||
assert mount in rendered["services"][module.SERVICE]["volumes"]
|
||||
assert rendered["volumes"][module.MANAGED_APPDATA_VOLUME] == {
|
||||
"name": module.MANAGED_APPDATA_VOLUME,
|
||||
"external": True,
|
||||
}
|
||||
|
||||
|
||||
def test_compose_rejects_a_managed_appdata_target_alias(compose):
|
||||
baseline, _calls = compose
|
||||
baseline["services"][module.SERVICE]["volumes"].append({
|
||||
"type": "volume",
|
||||
"source": module.MANAGED_APPDATA_VOLUME,
|
||||
"target": "/var/lib/docker/volumes/foreign/_data",
|
||||
})
|
||||
baseline["volumes"] = {
|
||||
module.MANAGED_APPDATA_VOLUME: {
|
||||
"name": module.MANAGED_APPDATA_VOLUME,
|
||||
"external": True,
|
||||
}
|
||||
}
|
||||
with pytest.raises(module.VolumeTransitionError, match="managed_appdata"):
|
||||
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
|
||||
|
||||
|
||||
def test_source_relative_controller_bind_uses_original_compose_directory(monkeypatch):
|
||||
def run(arguments, *, data=None, **kwargs):
|
||||
if arguments[:3] == ["docker", "image", "inspect"]:
|
||||
@@ -274,7 +544,14 @@ def test_source_relative_controller_bind_uses_original_compose_directory(monkeyp
|
||||
assert next(m["source"] for m in mounts if m["target"] == module.EXECUTABLE_TARGET) == module._mount_source_for_revision(REVISION)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("damage", ["target_collision", "foreign_volume", "graph_rw", "relative_bind", "env_file"])
|
||||
@pytest.mark.parametrize(
|
||||
"damage",
|
||||
[
|
||||
"target_collision", "foreign_volume", "graph_rw", "relative_bind", "env_file",
|
||||
"backup_target_collision", "backup_volume_alias", "backup_target_child_alias",
|
||||
"backup_definition_collision",
|
||||
],
|
||||
)
|
||||
def test_compose_rejects_unsafe_or_unpinned_activation_inputs(compose, damage):
|
||||
baseline, _ = compose
|
||||
if damage == "target_collision":
|
||||
@@ -289,12 +566,65 @@ def test_compose_rejects_unsafe_or_unpinned_activation_inputs(compose, damage):
|
||||
baseline["services"]["deploy-control"]["volumes"][0]["read_only"] = False
|
||||
elif damage == "relative_bind":
|
||||
baseline["services"]["runner"]["volumes"][0]["source"] = "./unsafe"
|
||||
else:
|
||||
elif damage == "env_file":
|
||||
baseline["services"]["deploy-control"]["env_file"] = [".env"]
|
||||
elif damage == "backup_target_collision":
|
||||
baseline["services"]["deploy-control"]["volumes"].append(
|
||||
{"type": "bind", "source": "/foreign", "target": "/forge-master-key-backup"}
|
||||
)
|
||||
elif damage == "backup_volume_alias":
|
||||
baseline["services"]["runner"]["volumes"].append(
|
||||
{
|
||||
"type": "volume", "source": "itworx-forge-master-key-backup",
|
||||
"target": "/foreign-backup",
|
||||
}
|
||||
)
|
||||
elif damage == "backup_target_child_alias":
|
||||
baseline["services"]["runner"]["volumes"].append(
|
||||
{"type": "bind", "source": "/foreign", "target": "/forge-master-key-backup/state"}
|
||||
)
|
||||
else:
|
||||
baseline["volumes"] = {
|
||||
"itworx-forge-master-key-backup": {"name": "foreign", "external": True}
|
||||
}
|
||||
with pytest.raises(module.VolumeTransitionError):
|
||||
module.rendered_model(ORIGINAL_COMPOSE, REVISION, IMAGE_ID)
|
||||
|
||||
|
||||
def test_pre_promotion_accepts_the_healthy_controller_without_the_new_mount(compose):
|
||||
baseline, _calls = compose
|
||||
reference = "registry.example/controller:stable"
|
||||
baseline["services"][module.SERVICE]["image"] = reference
|
||||
current = {
|
||||
"Image": IMAGE_ID,
|
||||
"Config": {
|
||||
"Image": reference,
|
||||
"Labels": {
|
||||
"com.docker.compose.project": "gitea-deploy-runners",
|
||||
"com.docker.compose.service": module.SERVICE,
|
||||
},
|
||||
},
|
||||
"State": {"Health": {"Status": "healthy"}},
|
||||
"Mounts": [
|
||||
{
|
||||
"Type": "volume", "Name": module.VOLUME,
|
||||
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET,
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
original = f"name: gitea-deploy-runners\nservices:\n deploy-control:\n image: {reference}\n".encode()
|
||||
rendered = json.loads(
|
||||
module.rendered_model(original, REVISION, IMAGE_ID, current_container=current)
|
||||
)
|
||||
assert not any(mount.get("Destination") == "/forge-master-key-backup" for mount in current["Mounts"])
|
||||
assert any(
|
||||
mount.get("target") == "/forge-master-key-backup"
|
||||
and mount.get("source") == "itworx-forge-master-key-backup"
|
||||
for mount in rendered["services"][module.SERVICE]["volumes"]
|
||||
)
|
||||
|
||||
|
||||
def test_compose_tag_replacement_cannot_change_controller_startup_image(compose, monkeypatch):
|
||||
_baseline, calls = compose
|
||||
original_run = module._run
|
||||
@@ -385,6 +715,184 @@ def test_config_snapshot_has_exact_allowlist_and_broker_token():
|
||||
assert config_dirs == directories
|
||||
|
||||
|
||||
def _forge_secret_entries():
|
||||
return {
|
||||
name: (f'fixture:{index}'.encode(), module.FORGE_SECRET_MODE)
|
||||
for index, name in enumerate(sorted(module.FORGE_SECRET_OWNERS))
|
||||
}
|
||||
|
||||
|
||||
def test_complete_forge_secret_bundle_has_exact_export_modes_and_owners():
|
||||
entries = {
|
||||
'projects.json': (b'{}', 0o600),
|
||||
'broker-token': (b'fixture-token', 0o600),
|
||||
'gitea-validation.json': (b'{}', 0o600),
|
||||
'gitea-validation-token': (b'fixture-validation-token', 0o600),
|
||||
} | _forge_secret_entries()
|
||||
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
|
||||
|
||||
config, config_dirs = module._config_tree(entries, directories)
|
||||
archive_bytes = module._release_tar_bytes(REVISION, (config, config_dirs))
|
||||
restored, restored_dirs = module._read_tar_tree(archive_bytes)
|
||||
|
||||
assert restored == config
|
||||
assert restored_dirs == config_dirs
|
||||
with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode='r:') as archive:
|
||||
for name, owner in module.FORGE_SECRET_OWNERS.items():
|
||||
member = archive.getmember(name)
|
||||
assert (member.uid, member.gid) == owner
|
||||
assert member.mode == module.FORGE_SECRET_MODE
|
||||
assert archive.getmember('broker-token').uid == 0
|
||||
assert archive.getmember('broker-token').gid == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('damage', ['partial', 'unknown', 'wrong_mode', 'nested'])
|
||||
def test_forge_secret_snapshot_requires_the_complete_exact_private_tree(damage):
|
||||
entries = {
|
||||
'projects.json': (b'{}', 0o600),
|
||||
'broker-token': (b'fixture-token', 0o600),
|
||||
'gitea-validation.json': (b'{}', 0o600),
|
||||
'gitea-validation-token': (b'fixture-validation-token', 0o600),
|
||||
} | _forge_secret_entries()
|
||||
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
|
||||
if damage == 'partial':
|
||||
entries.pop(min(module.FORGE_SECRET_OWNERS))
|
||||
elif damage == 'unknown':
|
||||
entries[f'{module.FORGE_SECRET_DIRECTORY}/unexpected.secret'] = (b'x', 0o400)
|
||||
elif damage == 'wrong_mode':
|
||||
name = min(module.FORGE_SECRET_OWNERS)
|
||||
entries[name] = (entries[name][0], 0o600)
|
||||
else:
|
||||
directories[f'{module.FORGE_SECRET_DIRECTORY}/nested'] = 0o700
|
||||
|
||||
with pytest.raises(module.VolumeTransitionError):
|
||||
module._config_tree(entries, directories)
|
||||
|
||||
|
||||
def test_forge_secret_tar_owner_drift_is_rejected():
|
||||
output = io.BytesIO()
|
||||
with tarfile.open(fileobj=output, mode='w', format=tarfile.USTAR_FORMAT) as archive:
|
||||
member = tarfile.TarInfo(
|
||||
f'{module.FORGE_SECRET_DIRECTORY}/runtime_database_url.secret'
|
||||
)
|
||||
member.size = 1
|
||||
member.mode = module.FORGE_SECRET_MODE
|
||||
member.uid = member.gid = 0
|
||||
archive.addfile(member, io.BytesIO(b'x'))
|
||||
|
||||
with pytest.raises(module.VolumeTransitionError, match='entry_unsafe'):
|
||||
module._read_tar_tree(output.getvalue())
|
||||
|
||||
|
||||
def test_config_writer_uses_archive_mode_to_preserve_forge_leaf_owners(monkeypatch):
|
||||
tree = (
|
||||
{
|
||||
'projects.json': (b'{}', 0o600),
|
||||
'broker-token': (b'fixture-token', 0o600),
|
||||
'gitea-validation.json': (b'{}', 0o600),
|
||||
'gitea-validation-token': (b'fixture-validation-token', 0o600),
|
||||
} | _forge_secret_entries(),
|
||||
{'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700},
|
||||
)
|
||||
writer_id = 'f' * 64
|
||||
commands = []
|
||||
copy_payloads = []
|
||||
|
||||
def run(arguments, *, data=None, **_kwargs):
|
||||
commands.append(arguments)
|
||||
if arguments == ['docker', 'create']:
|
||||
return writer_id.encode()
|
||||
if arguments[:2] == ['docker', 'wait']:
|
||||
return b'0'
|
||||
if arguments[:2] == ['docker', 'cp']:
|
||||
copy_payloads.append(data)
|
||||
return b''
|
||||
|
||||
monkeypatch.setattr(module, '_read_partial_config_leaf', lambda: None)
|
||||
monkeypatch.setattr(module, 'ensure_volume', lambda **_kwargs: None)
|
||||
monkeypatch.setattr(module, '_release_tar_bytes', lambda *_args, **_kwargs: b'archive')
|
||||
monkeypatch.setattr(module, '_remove_stale_writer', lambda *_args, **_kwargs: None)
|
||||
monkeypatch.setattr(module, '_writer_spec', lambda *_args, **_kwargs: ['docker', 'create'])
|
||||
monkeypatch.setattr(module, '_inspect_writer', lambda *_args, **_kwargs: {})
|
||||
monkeypatch.setattr(module, '_read_config_leaf', lambda: tree)
|
||||
monkeypatch.setattr(module, '_run', run)
|
||||
|
||||
module._install_config_tree(tree, IMAGE_ID)
|
||||
|
||||
assert [command for command in commands if command[:2] == ['docker', 'cp']] == [[
|
||||
'docker', 'cp', '-a', '-', f'{writer_id}:{module.TARGET}/{module.CONFIG_DIR}',
|
||||
]]
|
||||
assert copy_payloads == [b'archive']
|
||||
|
||||
|
||||
def test_partial_forge_bundle_fails_during_promotion_preparation_before_stop(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
valid_config_tree = module._config_tree
|
||||
events = _prepare_promotion_doubles(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(module, '_config_tree', valid_config_tree)
|
||||
partial = {
|
||||
'projects.json': (b'{}', 0o600),
|
||||
'broker-token': (b'fixture-token', 0o600),
|
||||
'gitea-validation.json': (b'{}', 0o600),
|
||||
'gitea-validation-token': (b'fixture-validation-token', 0o600),
|
||||
min(module.FORGE_SECRET_OWNERS): (b'fixture', module.FORGE_SECRET_MODE),
|
||||
}
|
||||
directories = {'': 0o755, module.FORGE_SECRET_DIRECTORY: 0o700}
|
||||
identity = {
|
||||
'sourceRevision': 'b' * 40,
|
||||
'sourceTree': 'c' * 40,
|
||||
'controllerVersion': '2.1.0',
|
||||
}
|
||||
monkeypatch.setattr(
|
||||
module, '_snapshot_attested_controller',
|
||||
lambda *_args, **_kwargs: (partial, directories, identity),
|
||||
)
|
||||
|
||||
with pytest.raises(module.VolumeTransitionError, match='forge_secret_bundle_incomplete'):
|
||||
module.prepare_promotion(
|
||||
tmp_path / 'stage',
|
||||
source_revision='d' * 40,
|
||||
source_tree='e' * 40,
|
||||
controller_version='2.2.0',
|
||||
image_id=IMAGE_ID,
|
||||
)
|
||||
|
||||
assert 'backup_volume_prepared' not in events
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == 'nt' or not hasattr(os, 'geteuid') or os.geteuid() != 0,
|
||||
reason='fixed Forge ownership roundtrip requires Linux root')
|
||||
def test_forge_secret_filesystem_capture_and_private_stage_keep_exact_owner(tmp_path):
|
||||
config_root = tmp_path / 'config'
|
||||
config_root.mkdir(mode=0o700)
|
||||
secret_root = config_root / module.FORGE_SECRET_DIRECTORY
|
||||
secret_root.mkdir(mode=0o700)
|
||||
for index, (relative, owner) in enumerate(sorted(module.FORGE_SECRET_OWNERS.items())):
|
||||
path = config_root / relative
|
||||
path.write_bytes(f'fixture:{index}'.encode())
|
||||
os.chown(path, *owner)
|
||||
path.chmod(module.FORGE_SECRET_MODE)
|
||||
|
||||
descriptor = os.open(config_root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
||||
try:
|
||||
entries, directories = module._read_tree_fd(descriptor, allow_forge_secrets=True)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
config, config_dirs = module._config_tree(entries, directories, allow_partial=True)
|
||||
payload = module._release_tar_bytes(REVISION, (config, config_dirs))
|
||||
roundtrip, roundtrip_dirs = module._read_tar_tree(payload)
|
||||
assert roundtrip == config
|
||||
assert roundtrip_dirs == config_dirs
|
||||
|
||||
staged = tmp_path / 'private-stage'
|
||||
module._write_private_tree(staged, config, config_dirs)
|
||||
for relative, owner in module.FORGE_SECRET_OWNERS.items():
|
||||
info = (staged / relative).stat(follow_symlinks=False)
|
||||
assert (info.st_uid, info.st_gid) == owner
|
||||
assert stat.S_IMODE(info.st_mode) == module.FORGE_SECRET_MODE
|
||||
|
||||
|
||||
def test_unknown_config_file_is_rejected():
|
||||
entries = {
|
||||
"projects.json": (b"{}", 0o600),
|
||||
@@ -655,6 +1163,154 @@ def test_installer_release_order_matches_renderer_contract():
|
||||
assert module.RELEASE_FILES[:3] == (
|
||||
"controller_paths.py", "mcp_hub_release_store.py", "hub_permission_preparation.py"
|
||||
)
|
||||
assert module.RELEASE_FILES[module.RELEASE_FILES.index("forge_first_install.py") + 1] == (
|
||||
"forge_database_credentials.py"
|
||||
)
|
||||
helper_index = module.RELEASE_FILES.index("forge_database_credentials.py") + 1
|
||||
assert module.RELEASE_FILES[helper_index : helper_index + 2] == (
|
||||
"forge_backup_paths.py", "forge_secret_bundle.py"
|
||||
)
|
||||
assert module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER == tuple(
|
||||
name for name in module.RELEASE_FILES
|
||||
if name not in {"forge_backup_paths.py", "forge_secret_bundle.py"}
|
||||
)
|
||||
assert module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER == tuple(
|
||||
name for name in module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER
|
||||
if name != "forge_database_credentials.py"
|
||||
)
|
||||
installer = (ROOT / "scripts/upgrade_deployment_controller.sh").read_text(encoding="utf-8")
|
||||
module_list = installer.split("module_files=(", 1)[1].split(")", 1)[0].split()
|
||||
module_helper_index = module_list.index("forge_database_credentials.py") + 1
|
||||
assert module_list[module_helper_index : module_helper_index + 2] == [
|
||||
"forge_backup_paths.py", "forge_secret_bundle.py"
|
||||
]
|
||||
source_list = installer.split("source_paths=(", 1)[1].split(")", 1)[0].split()
|
||||
source_helper_index = source_list.index("ops/gitea-deploy-broker/forge_database_credentials.py") + 1
|
||||
assert source_list[source_helper_index : source_helper_index + 2] == [
|
||||
"ops/gitea-deploy-broker/forge_backup_paths.py",
|
||||
"ops/gitea-deploy-broker/forge_secret_bundle.py",
|
||||
]
|
||||
|
||||
|
||||
def test_current_release_digest_binds_the_role_password_helper():
|
||||
entries = {name: (f"source:{name}".encode(), 0o644) for name in module.RELEASE_FILES}
|
||||
expected = module._artifact_digest(entries)
|
||||
assert module._matching_release_files(entries, expected) == module.RELEASE_FILES
|
||||
|
||||
entries["forge_database_credentials.py"] = (b"tampered helper", 0o644)
|
||||
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
|
||||
module._matching_release_files(entries, expected)
|
||||
|
||||
|
||||
def test_previous_role_helper_release_digest_remains_recognized_and_bound():
|
||||
files = module.LEGACY_RELEASE_FILES_WITH_ROLE_HELPER
|
||||
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
|
||||
expected = module._artifact_digest(entries, files)
|
||||
assert module._matching_release_files(entries, expected) == files
|
||||
|
||||
entries["forge_database_credentials.py"] = (b"tampered role helper", 0o644)
|
||||
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
|
||||
module._matching_release_files(entries, expected)
|
||||
|
||||
|
||||
def test_historical_config_root_manifest_order_and_digest_remain_exact():
|
||||
files = HISTORICAL_CONFIG_ROOT_RELEASE_FILES
|
||||
assert module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT == files
|
||||
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
|
||||
expected = module._artifact_digest(entries, files)
|
||||
assert module._matching_release_files(entries, expected) == files
|
||||
|
||||
entries["status_server.py"] = (b"tampered historical file", 0o644)
|
||||
with pytest.raises(module.VolumeTransitionError, match="release_identity_invalid"):
|
||||
module._matching_release_files(entries, expected)
|
||||
|
||||
|
||||
def test_activated_controller_must_have_the_exact_backup_mount(monkeypatch):
|
||||
mounts = [
|
||||
{
|
||||
"Type": "bind", "Source": module._expected_revision_path(REVISION).as_posix(),
|
||||
"Destination": module.EXECUTABLE_TARGET, "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "volume", "Name": module.VOLUME,
|
||||
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET, "RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "bind", "Source": module._expected_config_path().as_posix(),
|
||||
"Destination": module.CONFIG_TARGET, "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "bind", "Source": module.DOCKER_ROOT.as_posix(),
|
||||
"Destination": module.DOCKER_ROOT.as_posix(), "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "volume", "Name": "itworx-forge-master-key-backup",
|
||||
"Source": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
||||
"Destination": "/forge-master-key-backup", "RW": True,
|
||||
},
|
||||
]
|
||||
item = {"Mounts": mounts}
|
||||
monkeypatch.setattr(module, "_controller_image_inspect", lambda _image_id: item)
|
||||
monkeypatch.setattr(module, "_read_release_leaf", lambda _revision: ({}, {"": 0o755}))
|
||||
monkeypatch.setattr(module, "_release_identity", lambda _entries: {"sourceRevision": REVISION})
|
||||
monkeypatch.setattr(module, "_read_config_leaf", lambda: ({}, {"": 0o755}))
|
||||
|
||||
module._verify_active_mount(REVISION, IMAGE_ID)
|
||||
mounts.pop()
|
||||
with pytest.raises(module.VolumeTransitionError, match="backup_mount"):
|
||||
module._verify_active_mount(REVISION, IMAGE_ID)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"damage",
|
||||
["backup_readonly", "backup_name", "backup_source", "backup_alias", "docker_root_rw"],
|
||||
)
|
||||
def test_activated_controller_rejects_backup_or_docker_root_mount_drift(
|
||||
monkeypatch, damage
|
||||
):
|
||||
mounts = [
|
||||
{
|
||||
"Type": "bind", "Source": module._expected_revision_path(REVISION).as_posix(),
|
||||
"Destination": module.EXECUTABLE_TARGET, "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "volume", "Name": module.VOLUME,
|
||||
"Source": module.MOUNTPOINT.as_posix(), "Destination": module.TARGET, "RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "bind", "Source": module._expected_config_path().as_posix(),
|
||||
"Destination": module.CONFIG_TARGET, "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "bind", "Source": module.DOCKER_ROOT.as_posix(),
|
||||
"Destination": module.DOCKER_ROOT.as_posix(), "RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "volume", "Name": module.BACKUP_VOLUME,
|
||||
"Source": module.BACKUP_MOUNTPOINT.as_posix(),
|
||||
"Destination": module.BACKUP_TARGET, "RW": True,
|
||||
},
|
||||
]
|
||||
if damage == "backup_readonly":
|
||||
mounts[-1]["RW"] = False
|
||||
elif damage == "backup_name":
|
||||
mounts[-1]["Name"] = "foreign-volume"
|
||||
elif damage == "backup_source":
|
||||
mounts[-1]["Source"] = "/var/lib/docker/volumes/foreign/_data"
|
||||
elif damage == "backup_alias":
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": "/foreign", "Destination": f"{module.BACKUP_TARGET}/child",
|
||||
"RW": True,
|
||||
})
|
||||
else:
|
||||
mounts[-2]["RW"] = True
|
||||
monkeypatch.setattr(module, "_controller_image_inspect", lambda _image_id: {"Mounts": mounts})
|
||||
monkeypatch.setattr(module, "_read_release_leaf", lambda _revision: ({}, {"": 0o755}))
|
||||
monkeypatch.setattr(module, "_release_identity", lambda _entries: {"sourceRevision": REVISION})
|
||||
monkeypatch.setattr(module, "_read_config_leaf", lambda: ({}, {"": 0o755}))
|
||||
|
||||
with pytest.raises(module.VolumeTransitionError):
|
||||
module._verify_active_mount(REVISION, IMAGE_ID)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="dirfd swap test requires POSIX openat")
|
||||
@@ -943,6 +1599,10 @@ def test_capture_program_is_fixed_descriptor_based_and_compiles():
|
||||
@pytest.mark.parametrize("legacy_config", [True, False])
|
||||
def test_capture_archive_roundtrips_through_a_nonseekable_pipe(legacy_config):
|
||||
source = module._controller_capture_source(config_root_in_code=legacy_config)
|
||||
assert '__FORGE_' not in source
|
||||
assert 'runtime_database_url.secret' in source
|
||||
assert '/forge-master-key-backup' not in source
|
||||
compile(source, 'controller-capture', 'exec')
|
||||
statements = ast.parse(source).body
|
||||
start = next(i for i, node in enumerate(statements)
|
||||
if isinstance(node, ast.Assign)
|
||||
@@ -958,6 +1618,7 @@ def test_capture_archive_roundtrips_through_a_nonseekable_pipe(legacy_config):
|
||||
"import io,sys,tarfile\n"
|
||||
"records={'deploy.py':(b'fixture source',0o644),'secrets/key':(b'fixture secret',0o600)}\n"
|
||||
"dirs={'secrets':0o700}\n"
|
||||
"forge_secret_owners={}\n"
|
||||
)
|
||||
result = subprocess.run([sys.executable, "-c", fixture + archive_program],
|
||||
capture_output=True, timeout=10, check=False)
|
||||
@@ -971,8 +1632,13 @@ def test_capture_archive_roundtrips_through_a_nonseekable_pipe(legacy_config):
|
||||
assert all(member.uid == member.gid == 0 for member in archive.getmembers())
|
||||
|
||||
|
||||
def test_prior_release_capture_must_match_protected_git_tree(monkeypatch):
|
||||
files = module.LEGACY_RELEASE_FILES_2_1_5
|
||||
@pytest.mark.parametrize("files", [
|
||||
module.LEGACY_RELEASE_FILES_WITHOUT_ROLE_HELPER,
|
||||
module.LEGACY_RELEASE_FILES_2_1_5,
|
||||
module.LEGACY_RELEASE_FILES_WITH_CONFIG_ROOT,
|
||||
module.LEGACY_RELEASE_FILES_BEFORE_FORGE,
|
||||
])
|
||||
def test_prior_release_capture_must_match_protected_git_tree(monkeypatch, files):
|
||||
entries = {name: (f"source:{name}".encode(), 0o644) for name in files}
|
||||
source_revision = "32a6eece0e3e1276946abdc04196c819daf02e2a"
|
||||
source_tree = "e0d7e80b79504b964d968e51e0d21c288cac8f0d"
|
||||
|
||||
@@ -31,6 +31,10 @@ def test_windows_wrapper_archives_every_file_required_by_root_installer():
|
||||
)
|
||||
assert vendor_sources == required_vendor_sources
|
||||
assert "scripts/prepare_controller_hub_release_volume.py" in archived.group(1)
|
||||
assert "ops/gitea-deploy-broker/forge_first_install.py" in required_paths
|
||||
assert "ops/gitea-deploy-broker/forge_database_credentials.py" in required_paths
|
||||
assert "ops/gitea-deploy-broker/forge_backup_paths.py" in required_paths
|
||||
assert "ops/gitea-deploy-broker/forge_secret_bundle.py" in required_paths
|
||||
assert "scripts/upgrade_deployment_controller.sh" in archived.group(1)
|
||||
assert "scripts/vendor" in archived.group(1)
|
||||
assert "scripts/vendor/pyyaml-LICENSE" in required.group(1)
|
||||
@@ -39,6 +43,28 @@ def test_windows_wrapper_archives_every_file_required_by_root_installer():
|
||||
assert '"${work}/vendor/yaml/${vendor_file}"' in installer
|
||||
assert ('[[ -d "${script_dir}/vendor" && ! -L "${script_dir}/vendor" && '
|
||||
'-d "${script_dir}/vendor/yaml" && ! -L "${script_dir}/vendor/yaml" ]]') in installer
|
||||
assert "forge_first_install.py" in installer.split("module_files=(", 1)[1].split(")", 1)[0]
|
||||
assert "forge_database_credentials.py" in installer.split("module_files=(", 1)[1].split(")", 1)[0]
|
||||
assert "forge_backup_paths.py" in installer.split("module_files=(", 1)[1].split(")", 1)[0]
|
||||
assert "forge_secret_bundle.py" in installer.split("module_files=(", 1)[1].split(")", 1)[0]
|
||||
|
||||
|
||||
def test_forge_first_install_module_is_bound_into_controller_release_and_config():
|
||||
renderer = (ROOT / "scripts/prepare_controller_artifact.py").read_text(encoding="utf-8")
|
||||
preparer = (ROOT / "scripts/prepare_controller_hub_release_volume.py").read_text(encoding="utf-8")
|
||||
|
||||
assert '"forge_first_install.py"' in renderer.split("_ARTIFACT_FILES = (", 1)[1].split(")", 1)[0]
|
||||
release_files = preparer.split("RELEASE_FILES = (", 1)[1].split(")", 1)[0]
|
||||
legacy_role_release_files = preparer.split(
|
||||
"LEGACY_RELEASE_FILES_WITH_ROLE_HELPER = (", 1
|
||||
)[1].split(")", 1)[0]
|
||||
assert "'forge_first_install.py'" in legacy_role_release_files
|
||||
assert "'forge_database_credentials.py'" in legacy_role_release_files
|
||||
assert "*LEGACY_RELEASE_FILES_WITH_ROLE_HELPER" in release_files
|
||||
assert "'forge_backup_paths.py'" in release_files
|
||||
assert "'forge_secret_bundle.py'" in release_files
|
||||
assert "'forge-first-install.json'" in preparer.split("CONFIG_FILES = {", 1)[1].split("}", 1)[0]
|
||||
assert "'forge-bootstrap-secrets'" in preparer.split("CONFIG_SECRET_DIRS = {", 1)[1].split("}", 1)[0]
|
||||
|
||||
|
||||
def test_windows_wrapper_supplies_private_archive_and_exact_image_identity():
|
||||
|
||||
@@ -192,6 +192,8 @@ def test_upgrade_prepares_before_stop_and_reconciles_between_stop_and_activation
|
||||
activate = 'python3 "${work}/${PREPARER_NAME}" activate --backup "$backup"'
|
||||
|
||||
assert '/srv/example/appdata/gitea-deploy-runners/deploy.lock' not in script
|
||||
assert 'lock_path="${ROOT}/promotion.lock"' in script
|
||||
assert 'lock_path="${VOLUME_ROOT}/promotion.lock"' not in script
|
||||
assert script.index(prepare) < script.index(stop)
|
||||
assert script.index(stop) < script.index(reconcile)
|
||||
assert script.index(reconcile) < script.index(activate)
|
||||
|
||||
@@ -7001,6 +7001,12 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
|
||||
renderer = importlib.util.module_from_spec(renderer_spec)
|
||||
renderer_spec.loader.exec_module(renderer)
|
||||
assert renderer._ARTIFACT_FILES == status_module._CONTROLLER_ARTIFACT_FILES
|
||||
forge_first_install_index = status_module._CONTROLLER_ARTIFACT_FILES.index(
|
||||
"forge_first_install.py"
|
||||
)
|
||||
assert status_module._CONTROLLER_ARTIFACT_FILES[forge_first_install_index + 1] == (
|
||||
"forge_database_credentials.py"
|
||||
)
|
||||
artifact_root = tmp_path / "controller"
|
||||
artifact_root.mkdir()
|
||||
for filename in status_module._CONTROLLER_ARTIFACT_FILES:
|
||||
|
||||
@@ -0,0 +1,339 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE_PATH = ROOT / "ops/gitea-deploy-broker/forge_backup_paths.py"
|
||||
if MODULE_PATH.is_file():
|
||||
spec = importlib.util.spec_from_file_location("forge_backup_paths_test", MODULE_PATH)
|
||||
assert spec is not None and spec.loader is not None
|
||||
paths = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(paths)
|
||||
else:
|
||||
paths = None
|
||||
|
||||
|
||||
def _paths():
|
||||
if paths is None:
|
||||
pytest.fail("fixed Forge backup path verifier has not been implemented", pytrace=False)
|
||||
return paths
|
||||
|
||||
|
||||
def volume_metadata():
|
||||
return {
|
||||
"Name": "itworx-forge-master-key-backup",
|
||||
"Driver": "local",
|
||||
"Scope": "local",
|
||||
"Options": None,
|
||||
"Labels": {
|
||||
"itworx.managed-by": "deployment-controller",
|
||||
"itworx.purpose": "forge-master-key-backup",
|
||||
},
|
||||
"Mountpoint": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
||||
}
|
||||
|
||||
|
||||
def controller_mounts():
|
||||
return [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "itworx-forge-master-key-backup",
|
||||
"Driver": "local",
|
||||
"Source": "/var/lib/docker/volumes/itworx-forge-master-key-backup/_data",
|
||||
"Destination": "/forge-master-key-backup",
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Name": "",
|
||||
"Source": "/var/lib/docker",
|
||||
"Destination": "/var/lib/docker",
|
||||
"RW": False,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "itworx-managed-appdata",
|
||||
"Driver": "local",
|
||||
"Source": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
||||
"Destination": "/var/lib/docker/volumes/itworx-managed-appdata/_data",
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "itworx-controller-hub-releases",
|
||||
"Driver": "local",
|
||||
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data",
|
||||
"Destination": "/controller-hub-releases",
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Name": "",
|
||||
"Source": "/var/lib/docker/volumes/itworx-controller-hub-releases/_data/controller-config",
|
||||
"Destination": "/controller-config",
|
||||
"RW": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def mountinfo(*, backup_options="rw,nosuid,nodev", docker_options="ro,relatime"):
|
||||
base = [
|
||||
"35 25 8:1 / / rw,relatime - ext4 /dev/sda1 rw,relatime",
|
||||
f"41 35 8:2 / /var/lib/docker {docker_options} - ext4 /dev/sda2 ro,relatime",
|
||||
f"42 35 8:3 / /forge-master-key-backup {backup_options} - ext4 /dev/sda3 rw,relatime",
|
||||
]
|
||||
base.extend(
|
||||
f"{50 + index} 41 8:{index + 4} / /var/lib/docker/containers/c{index}/mount{index} "
|
||||
f"{'ro' if index % 2 else 'rw'},relatime - ext4 /dev/sdb{index} rw,relatime"
|
||||
for index in range(35)
|
||||
)
|
||||
return "\n".join(base)
|
||||
|
||||
|
||||
def test_exact_local_backup_volume_metadata_is_accepted():
|
||||
module = _paths()
|
||||
assert module.validate_volume_metadata(volume_metadata(), "/var/lib/docker") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
[
|
||||
("Name", "foreign"),
|
||||
("Driver", "nfs"),
|
||||
("Scope", "global"),
|
||||
("Options", {"device": "/mnt/share"}),
|
||||
("Options", []),
|
||||
("Labels", {"itworx.managed-by": "deployment-controller"}),
|
||||
("Labels", {
|
||||
"itworx.managed-by": "deployment-controller",
|
||||
"itworx.purpose": "managed-appdata",
|
||||
}),
|
||||
("Mountpoint", "/mnt/foreign"),
|
||||
],
|
||||
)
|
||||
def test_backup_volume_metadata_drift_is_rejected(field, value):
|
||||
module = _paths()
|
||||
item = volume_metadata()
|
||||
item[field] = value
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module.validate_volume_metadata(item, "/var/lib/docker")
|
||||
|
||||
|
||||
def test_backup_volume_metadata_requires_the_fixed_docker_root():
|
||||
module = _paths()
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module.validate_volume_metadata(volume_metadata(), "/mnt/docker")
|
||||
|
||||
|
||||
def test_mount_contract_allows_only_the_fixed_rw_backup_and_ro_docker_root():
|
||||
module = _paths()
|
||||
assert module.validate_controller_mounts(
|
||||
controller_mounts(),
|
||||
controller_id_before="a" * 64,
|
||||
controller_id_after="a" * 64,
|
||||
) is None
|
||||
|
||||
|
||||
def test_mount_contract_allows_managed_appdata_to_be_absent():
|
||||
module = _paths()
|
||||
mounts = controller_mounts()
|
||||
mounts.pop(2)
|
||||
assert module.validate_controller_mounts(
|
||||
mounts,
|
||||
controller_id_before="a" * 64,
|
||||
controller_id_after="a" * 64,
|
||||
) is None
|
||||
|
||||
|
||||
def test_mount_contract_allows_only_the_exact_managed_appdata_nested_volume():
|
||||
module = _paths()
|
||||
mounts = controller_mounts()
|
||||
module.validate_controller_mounts(
|
||||
mounts,
|
||||
controller_id_before="a" * 64,
|
||||
controller_id_after="a" * 64,
|
||||
)
|
||||
mounts[-3]["Destination"] = "/var/lib/docker/volumes/foreign/_data"
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module.validate_controller_mounts(
|
||||
mounts,
|
||||
controller_id_before="a" * 64,
|
||||
controller_id_after="a" * 64,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"damage",
|
||||
[
|
||||
"controller_changed",
|
||||
"backup_rw_disabled",
|
||||
"backup_name_changed",
|
||||
"backup_source_changed",
|
||||
"backup_destination_changed",
|
||||
"backup_driver_changed",
|
||||
"duplicate_backup_mount",
|
||||
"backup_source_alias",
|
||||
"backup_target_child_alias",
|
||||
"backup_target_parent_alias",
|
||||
"docker_root_rw",
|
||||
"docker_root_child_mount",
|
||||
"managed_appdata_duplicate",
|
||||
"managed_appdata_source_changed",
|
||||
"managed_appdata_driver_changed",
|
||||
"managed_appdata_target_alias",
|
||||
"managed_appdata_name_alias",
|
||||
"root_exposure",
|
||||
],
|
||||
)
|
||||
def test_mount_contract_rejects_aliases_and_mount_drift(damage):
|
||||
module = _paths()
|
||||
mounts = controller_mounts()
|
||||
before = after = "a" * 64
|
||||
if damage == "controller_changed":
|
||||
after = "b" * 64
|
||||
elif damage == "backup_rw_disabled":
|
||||
mounts[0]["RW"] = False
|
||||
elif damage == "backup_name_changed":
|
||||
mounts[0]["Name"] = "foreign"
|
||||
elif damage == "backup_source_changed":
|
||||
mounts[0]["Source"] = "/mnt/foreign"
|
||||
elif damage == "backup_destination_changed":
|
||||
mounts[0]["Destination"] = "/backup-copy"
|
||||
elif damage == "backup_driver_changed":
|
||||
mounts[0]["Driver"] = "nfs"
|
||||
elif damage == "duplicate_backup_mount":
|
||||
mounts.append(dict(mounts[0]))
|
||||
elif damage == "backup_source_alias":
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": volume_metadata()["Mountpoint"],
|
||||
"Destination": "/backup-copy", "RW": False,
|
||||
})
|
||||
elif damage == "backup_target_child_alias":
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": "/foreign", "Destination": "/forge-master-key-backup/state",
|
||||
"RW": True,
|
||||
})
|
||||
elif damage == "backup_target_parent_alias":
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": "/foreign", "Destination": "/",
|
||||
"RW": True,
|
||||
})
|
||||
elif damage == "docker_root_rw":
|
||||
mounts[1]["RW"] = True
|
||||
elif damage == "docker_root_child_mount":
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": "/var/lib/docker/volumes/other",
|
||||
"Destination": "/var/lib/docker/volumes/other", "RW": True,
|
||||
})
|
||||
elif damage == "managed_appdata_duplicate":
|
||||
mounts.append(dict(mounts[2]))
|
||||
elif damage == "managed_appdata_source_changed":
|
||||
mounts[2]["Source"] = "/var/lib/docker/volumes/foreign/_data"
|
||||
elif damage == "managed_appdata_driver_changed":
|
||||
mounts[2]["Driver"] = "nfs"
|
||||
elif damage == "managed_appdata_target_alias":
|
||||
mounts[2]["Destination"] = "/appdata-copy"
|
||||
elif damage == "managed_appdata_name_alias":
|
||||
mounts[2].update({
|
||||
"Source": "/var/lib/docker/volumes/foreign/_data",
|
||||
"Destination": "/appdata-copy",
|
||||
})
|
||||
else:
|
||||
mounts.append({
|
||||
"Type": "bind", "Source": "/", "Destination": "/", "RW": True,
|
||||
})
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module.validate_controller_mounts(
|
||||
mounts, controller_id_before=before, controller_id_after=after
|
||||
)
|
||||
|
||||
|
||||
def test_proc_mountinfo_requires_the_exact_rw_backup_and_ro_docker_mounts():
|
||||
module = _paths()
|
||||
assert module._validate_mountinfo_contract(mountinfo()) is None
|
||||
for unsafe in (
|
||||
mountinfo(backup_options="ro,nosuid,nodev"),
|
||||
mountinfo(docker_options="rw,relatime"),
|
||||
mountinfo().replace("/forge-master-key-backup", "/backup-copy"),
|
||||
mountinfo() + "\n43 35 8:4 / /forge-master-key-backup rw - ext4 /dev/sda4 rw",
|
||||
):
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module._validate_mountinfo_contract(unsafe)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="directory inode checks require POSIX descriptors")
|
||||
def test_directory_identity_binds_backup_mount_to_the_fixed_docker_volume_source(tmp_path):
|
||||
module = _paths()
|
||||
source = tmp_path / "source"
|
||||
mounted = tmp_path / "mounted"
|
||||
source.mkdir()
|
||||
mounted.mkdir()
|
||||
source_fd = os.open(source, os.O_RDONLY)
|
||||
mounted_fd = os.open(mounted, os.O_RDONLY)
|
||||
same_fd = os.open(source, os.O_RDONLY)
|
||||
try:
|
||||
module._require_same_directory(source_fd, same_fd)
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume, match="mount_source_mismatch"):
|
||||
module._require_same_directory(source_fd, mounted_fd)
|
||||
finally:
|
||||
os.close(source_fd)
|
||||
os.close(mounted_fd)
|
||||
os.close(same_fd)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
||||
reason="directory ownership checks require root on POSIX",
|
||||
)
|
||||
def test_fixed_directory_walk_rejects_symlinks_and_unsafe_modes(tmp_path):
|
||||
module = _paths()
|
||||
volume_root = tmp_path / "volume"
|
||||
volume_root.mkdir(mode=0o755)
|
||||
volume_root.chmod(0o755)
|
||||
fd = module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
||||
os.close(fd)
|
||||
|
||||
linked = tmp_path / "linked"
|
||||
linked.symlink_to(volume_root, target_is_directory=True)
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module._open_directory_no_symlinks(linked, anchor=tmp_path)
|
||||
|
||||
volume_root.chmod(0o777)
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name == "nt" or not hasattr(os, "geteuid") or os.geteuid() != 0,
|
||||
reason="directory ACL checks require root on POSIX",
|
||||
)
|
||||
def test_fixed_directory_walk_rejects_access_acl(tmp_path, monkeypatch):
|
||||
module = _paths()
|
||||
volume_root = tmp_path / "volume"
|
||||
volume_root.mkdir(mode=0o755)
|
||||
volume_root.chmod(0o755)
|
||||
monkeypatch.setattr(module.os, "listxattr", lambda _fd: ["system.posix_acl_access"])
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module._open_directory_no_symlinks(volume_root, anchor=tmp_path)
|
||||
|
||||
|
||||
def test_read_only_identity_check_fails_closed_without_creating_or_repairing_paths(tmp_path, monkeypatch):
|
||||
module = _paths()
|
||||
sentinel = tmp_path / "unchanged"
|
||||
sentinel.write_bytes(b"keep")
|
||||
bad = volume_metadata()
|
||||
bad["Labels"]["itworx.purpose"] = "foreign-purpose"
|
||||
monkeypatch.setattr(module, "_read_volume_snapshot", lambda: (bad, "/var/lib/docker"))
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"_open_fixed_backup_root",
|
||||
lambda: pytest.fail("unsafe volume metadata must fail before opening the data root"),
|
||||
)
|
||||
with pytest.raises(module.UnsafeForgeBackupVolume):
|
||||
module.validate_forge_backup_volume()
|
||||
assert sentinel.read_bytes() == b"keep"
|
||||
assert sorted(path.name for path in tmp_path.iterdir()) == ["unchanged"]
|
||||
File diff suppressed because it is too large.
Load diff
@@ -177,6 +177,11 @@ def test_forge_host_preflight_compares_exact_main_and_reports_fixed_runtime_chec
|
||||
monkeypatch.setattr(Path, "lstat", lstat_with_fixed_service_owners)
|
||||
monkeypatch.setattr(status_module, "_forge_gitea_main_revision", lambda: revision)
|
||||
monkeypatch.setattr(status_module, "_forge_push_validation", lambda _revision: "unknown")
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_load_forge_bootstrap_config",
|
||||
lambda _path: {"private_package_owner": "itworx-forge-private"},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module, "FORGE_COMPOSE_SHA256", hashlib.sha256(manifest).hexdigest()
|
||||
)
|
||||
@@ -217,13 +222,14 @@ def test_forge_host_preflight_compares_exact_main_and_reports_fixed_runtime_chec
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
result = status_module._forge_bootstrap_runtime_checks(profile)
|
||||
assert result["status"] == "preflight_unknown"
|
||||
assert result["status"] == "preflight_blocked"
|
||||
assert result["checks"]["exact_main"] == {"status": "passed"}
|
||||
assert result["checks"]["source_manifest"] == {"status": "passed"}
|
||||
assert result["checks"]["managed_appdata"] == {"status": "passed"}
|
||||
assert result["checks"]["proxy_network"] == {"status": "passed"}
|
||||
assert result["checks"]["resource_collisions"] == {"status": "passed"}
|
||||
assert result["checks"]["push_validation"] == {"status": "unknown"}
|
||||
assert result["checks"]["secret_delivery"] == {"status": "blocked"}
|
||||
assert result["evidence"]["mainRevision"] == revision
|
||||
assert result["evidence"]["sourceManifestSha256"] == hashlib.sha256(manifest).hexdigest()
|
||||
assert result["evidence"]["workspaceImagePresent"] is True
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.db.models import DeploymentJob
|
||||
from backend.app.schemas.enrollment import EnrollmentTrustConfigV1
|
||||
from backend.app.schemas.forge_bootstrap import ForgeBootstrapPreflightV1
|
||||
@@ -10,6 +11,9 @@ from backend.app.services.forge_bootstrap_preflight import (
|
||||
ForgeBootstrapPreflightService,
|
||||
)
|
||||
|
||||
_PRIVATE_OWNER = "itworx-forge-private"
|
||||
_SOURCE_URL = "https://gitea.example.com/Jens/itworx-forge"
|
||||
|
||||
|
||||
def _profile(**overrides):
|
||||
value = {
|
||||
@@ -58,6 +62,67 @@ def test_forge_profile_is_strict_and_cannot_select_runtime_inputs():
|
||||
raise AssertionError(f"invalid {field} must be rejected")
|
||||
|
||||
|
||||
def test_preflight_registry_attestation_binds_protected_owner_and_both_image_labels(monkeypatch):
|
||||
from backend.app.services.forge_bootstrap_preflight import (
|
||||
_checked_controller_result,
|
||||
)
|
||||
|
||||
profile = _profile()
|
||||
checks = {
|
||||
name: {"status": "unknown"}
|
||||
for name in (
|
||||
"exact_main", "source_manifest", "managed_appdata", "proxy_network",
|
||||
"resource_collisions", "push_validation", "image_registry", "oidc",
|
||||
"secret_delivery",
|
||||
)
|
||||
}
|
||||
checks["image_registry"] = {"status": "passed"}
|
||||
registry = {
|
||||
"owner": _PRIVATE_OWNER,
|
||||
"ownerVisibility": "private",
|
||||
"workspaceDigest": profile.workspace_image_digest,
|
||||
"frontdoorDigest": profile.frontdoor_image_digest,
|
||||
"workspacePullable": True,
|
||||
"frontdoorPullable": True,
|
||||
"workspaceSource": _SOURCE_URL,
|
||||
"workspaceRevision": profile.revision,
|
||||
"frontdoorSource": _SOURCE_URL,
|
||||
"frontdoorRevision": profile.revision,
|
||||
}
|
||||
result = {
|
||||
"apiVersion": "itworx.forge-bootstrap-preflight/v1",
|
||||
"status": "preflight_unknown",
|
||||
"revision": profile.revision,
|
||||
"workspaceImageDigest": profile.workspace_image_digest,
|
||||
"frontdoorImageDigest": profile.frontdoor_image_digest,
|
||||
"checks": checks,
|
||||
"evidence": {"attestations": {"image_registry": registry}},
|
||||
}
|
||||
monkeypatch.setitem(settings.apis, "FORGE_PRIVATE_PACKAGE_OWNER", _PRIVATE_OWNER)
|
||||
accepted = _checked_controller_result(result, profile)
|
||||
assert accepted is not None
|
||||
assert accepted[1]["attestations"]["image_registry"] == registry
|
||||
|
||||
for protected_owner in (None, "Jens", "owner.with.dot", "a" * 40):
|
||||
monkeypatch.setitem(settings.apis, "FORGE_PRIVATE_PACKAGE_OWNER", protected_owner)
|
||||
assert _checked_controller_result(result, profile) is None
|
||||
monkeypatch.setitem(settings.apis, "FORGE_PRIVATE_PACKAGE_OWNER", _PRIVATE_OWNER)
|
||||
|
||||
for field, invalid in (
|
||||
("owner", "another-owner"),
|
||||
("workspaceSource", "https://attacker.invalid/repo"),
|
||||
("frontdoorRevision", "d" * 40),
|
||||
("workspaceDigest", profile.frontdoor_image_digest),
|
||||
("workspacePullable", 1),
|
||||
("frontdoorPullable", 1),
|
||||
("workspacePullable", "true"),
|
||||
("frontdoorPullable", None),
|
||||
):
|
||||
bad = {**registry, field: invalid}
|
||||
result["evidence"]["attestations"]["image_registry"] = bad
|
||||
assert _checked_controller_result(result, profile) is None
|
||||
|
||||
|
||||
class _Broker:
|
||||
def __init__(self, result):
|
||||
self.result = result
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,487 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BROKER = ROOT / "ops" / "gitea-deploy-broker"
|
||||
if str(BROKER) not in sys.path:
|
||||
sys.path.insert(0, str(BROKER))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def credentials_module():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"forge_database_credentials_test",
|
||||
BROKER / "forge_database_credentials.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _secrets(module):
|
||||
return {
|
||||
module.RUNTIME_ROLE: "runtime-secret-SENTINEL-0123456789abcdef",
|
||||
module.MIGRATOR_ROLE: "migrator-secret-SENTINEL-fedcba9876543210",
|
||||
}
|
||||
|
||||
|
||||
def _uri(role: str, password: str) -> bytes:
|
||||
return f"postgresql://{role}:{password}@postgres:5432/forge".encode("ascii")
|
||||
|
||||
|
||||
def _install_fake_docker(
|
||||
module,
|
||||
tmp_path: Path,
|
||||
secrets: dict[str, str],
|
||||
*,
|
||||
behavior: str = "success",
|
||||
):
|
||||
marker = tmp_path / "fake-docker-state.txt"
|
||||
script = tmp_path / "fake-docker"
|
||||
secret_literals = json.dumps(secrets)
|
||||
marker_literal = json.dumps(str(marker))
|
||||
script.write_text(
|
||||
f"#!{sys.executable}\n"
|
||||
"import json, os, sys, termios, time\n"
|
||||
f"expected = {secret_literals}\n"
|
||||
f"marker = {marker_literal}\n"
|
||||
f"behavior = {behavior!r}\n"
|
||||
"args = sys.argv[1:]\n"
|
||||
"try:\n"
|
||||
" role_arg = args[args.index('-c') + 1]\n"
|
||||
" role = role_arg.split(' ', 1)[1]\n"
|
||||
"except Exception:\n"
|
||||
" open(marker, 'a').write('bad-command\\n')\n"
|
||||
" raise SystemExit(90)\n"
|
||||
f"expected_args = ['exec', '-i', '-t', '--user', '70:70', '--env', 'LANG=C', '--env', 'LC_ALL=C', '--env', 'PGPASSFILE=/dev/null', '--env', 'PGSERVICEFILE=/dev/null', '--env', 'PGPASSWORD=', '--env', 'PGOPTIONS=', 'itworx-forge-postgres-1', '/bin/busybox', 'timeout', '-s', 'TERM', '-k', '{module.REMOTE_KILL_AFTER_SECONDS}', '{module.REMOTE_TIMEOUT_SECONDS}', 'psql', '-X', '-w', '-v', 'ON_ERROR_STOP=1', '-h', '/var/run/postgresql', '-p', '5432', '-U', 'forge_bootstrap', '-d', 'forge', '-c', role_arg]\n"
|
||||
"if args != expected_args or role not in expected:\n"
|
||||
" open(marker, 'a').write('bad-command\\n')\n"
|
||||
" raise SystemExit(91)\n"
|
||||
"secret = expected[role]\n"
|
||||
"if any(secret in value for value in sys.argv + list(os.environ.values())):\n"
|
||||
" open(marker, 'a').write('secret-in-command-or-env\\n')\n"
|
||||
" raise SystemExit(92)\n"
|
||||
"if termios.tcgetattr(0)[3] & (termios.ECHO | termios.ECHONL):\n"
|
||||
" open(marker, 'a').write('echo-enabled\\n')\n"
|
||||
" raise SystemExit(93)\n"
|
||||
"if behavior == 'exit-before-prompt':\n"
|
||||
" raise SystemExit(7)\n"
|
||||
"if behavior == 'timeout-before-prompt':\n"
|
||||
" open(marker + '.pid', 'w').write(str(os.getpid()))\n"
|
||||
" time.sleep(30)\n"
|
||||
" raise SystemExit(0)\n"
|
||||
"if behavior == 'output-flood':\n"
|
||||
" os.write(2, b'x' * 65536)\n"
|
||||
" time.sleep(30)\n"
|
||||
" raise SystemExit(0)\n"
|
||||
"if behavior == 'prompts-then-exit':\n"
|
||||
" os.write(2, f'Enter new password for user \\\"{role}\\\": Enter it again: '.encode())\n"
|
||||
" raise SystemExit(0)\n"
|
||||
"os.write(2, f'Enter new password for user \\\"{role}\\\": '.encode())\n"
|
||||
"first = sys.stdin.readline().rstrip('\\r\\n')\n"
|
||||
"if first != secret:\n"
|
||||
" open(marker, 'a').write('first-mismatch\\n')\n"
|
||||
" raise SystemExit(94)\n"
|
||||
"if behavior == 'echo-received':\n"
|
||||
" os.write(2, first.encode() + b'\\n')\n"
|
||||
"if behavior == 'exit-after-first':\n"
|
||||
" raise SystemExit(8)\n"
|
||||
"os.write(2, b'Enter it again: ')\n"
|
||||
"second = sys.stdin.readline().rstrip('\\r\\n')\n"
|
||||
"if second != secret:\n"
|
||||
" open(marker, 'a').write('second-mismatch\\n')\n"
|
||||
" raise SystemExit(95)\n"
|
||||
"open(marker, 'a').write(role + '=matched-noecho\\n')\n"
|
||||
"raise SystemExit(0)\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
script.chmod(0o700)
|
||||
module._DOCKER_CLI = str(script)
|
||||
module._CHILD_PATH = str(tmp_path)
|
||||
return marker
|
||||
|
||||
|
||||
def _install_uri_reader(monkeypatch, module, secrets):
|
||||
uri_by_name = {
|
||||
"runtime_database_url": _uri(module.RUNTIME_ROLE, secrets[module.RUNTIME_ROLE]),
|
||||
"migrator_database_url": _uri(module.MIGRATOR_ROLE, secrets[module.MIGRATOR_ROLE]),
|
||||
}
|
||||
reads = []
|
||||
|
||||
def read(_root, name, role):
|
||||
reads.append((name, role))
|
||||
return uri_by_name.get(name)
|
||||
|
||||
monkeypatch.setattr(
|
||||
module,
|
||||
"read_database_uri",
|
||||
read,
|
||||
)
|
||||
return reads
|
||||
|
||||
|
||||
def _journal(initial):
|
||||
states = dict(initial)
|
||||
calls: list[tuple[str, str]] = []
|
||||
|
||||
def role_state(role):
|
||||
return states[role]
|
||||
|
||||
def transition(name, allowed, destination):
|
||||
def apply(role):
|
||||
if states[role] not in allowed:
|
||||
raise RuntimeError("compare-and-swap rejected")
|
||||
states[role] = destination
|
||||
calls.append((name, role))
|
||||
return True
|
||||
|
||||
return apply
|
||||
|
||||
return states, calls, {
|
||||
"role_state": role_state,
|
||||
"persist_role_intent": transition("intent", {"pending", "retryable"}, "intent"),
|
||||
"persist_role_retryable": transition("retryable", {"intent"}, "retryable"),
|
||||
"persist_role_unknown": transition("unknown", {"intent"}, "unknown"),
|
||||
"persist_role_complete": transition("complete", {"intent"}, "completed"),
|
||||
}
|
||||
|
||||
|
||||
def _run(module, secret_root, journal):
|
||||
return module.set_forge_database_role_passwords(secret_root, **journal)
|
||||
|
||||
|
||||
def test_fixed_roles_are_set_through_hidden_prompts_without_secret_output(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
reads = _install_uri_reader(monkeypatch, module, secrets)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets, behavior="echo-received")
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result == {
|
||||
"status": "completed",
|
||||
"completedRoles": [module.RUNTIME_ROLE, module.MIGRATOR_ROLE],
|
||||
"activeRole": None,
|
||||
}
|
||||
assert states == {
|
||||
module.RUNTIME_ROLE: "completed",
|
||||
module.MIGRATOR_ROLE: "completed",
|
||||
}
|
||||
assert calls == [
|
||||
("intent", module.RUNTIME_ROLE),
|
||||
("complete", module.RUNTIME_ROLE),
|
||||
("intent", module.MIGRATOR_ROLE),
|
||||
("complete", module.MIGRATOR_ROLE),
|
||||
]
|
||||
assert reads == [
|
||||
("runtime_database_url", module.RUNTIME_ROLE),
|
||||
("migrator_database_url", module.MIGRATOR_ROLE),
|
||||
]
|
||||
assert marker.read_text(encoding="utf-8").splitlines() == [
|
||||
f"{module.RUNTIME_ROLE}=matched-noecho",
|
||||
f"{module.MIGRATOR_ROLE}=matched-noecho",
|
||||
]
|
||||
serialized = json.dumps(result)
|
||||
assert all(secret not in serialized for secret in secrets.values())
|
||||
|
||||
|
||||
def test_completed_role_is_skipped_and_uncertain_role_is_never_replayed(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "completed", module.MIGRATOR_ROLE: "intent"}
|
||||
)
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] == "outcome_unknown"
|
||||
assert result["completedRoles"] == [module.RUNTIME_ROLE]
|
||||
assert result["activeRole"] == module.MIGRATOR_ROLE
|
||||
assert calls == []
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_migrator_cannot_be_journaled_complete_before_runtime_role(
|
||||
credentials_module, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "completed"}
|
||||
)
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert result["reason"] == "role_journal_order_invalid"
|
||||
assert result["completedRoles"] == []
|
||||
assert calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("behavior", ["exit-before-prompt", "timeout-before-prompt", "output-flood"])
|
||||
def test_no_input_failures_are_bounded_reaped_and_retryable(
|
||||
credentials_module, monkeypatch, tmp_path, behavior
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
monkeypatch.setattr(module, "PROMPT_TIMEOUT_SECONDS", 0.15)
|
||||
monkeypatch.setattr(module, "ROLE_TIMEOUT_SECONDS", 0.6)
|
||||
monkeypatch.setattr(module, "REMOTE_TIMEOUT_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_KILL_AFTER_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_TERMINATION_MARGIN_SECONDS", 0.1)
|
||||
monkeypatch.setattr(module, "MAX_TRANSCRIPT_BYTES", 128)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets, behavior=behavior)
|
||||
started = __import__("time").monotonic()
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
elapsed = __import__("time").monotonic() - started
|
||||
assert result["status"] == "failed"
|
||||
assert result["reason"] == "psql_failed_before_password_input"
|
||||
assert result["activeRole"] == module.RUNTIME_ROLE
|
||||
assert states[module.RUNTIME_ROLE] == "retryable"
|
||||
assert states[module.MIGRATOR_ROLE] == "pending"
|
||||
assert calls == [
|
||||
("intent", module.RUNTIME_ROLE),
|
||||
("retryable", module.RUNTIME_ROLE),
|
||||
]
|
||||
assert elapsed < 3
|
||||
if behavior == "timeout-before-prompt":
|
||||
assert elapsed >= 1.8
|
||||
child_pid = int(Path(str(marker) + ".pid").read_text(encoding="utf-8"))
|
||||
with pytest.raises(ProcessLookupError):
|
||||
os.kill(child_pid, 0)
|
||||
assert not marker.exists() or not any(
|
||||
value in marker.read_text(encoding="utf-8")
|
||||
for value in secrets.values()
|
||||
)
|
||||
|
||||
|
||||
def test_any_password_input_without_success_becomes_unknown(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
monkeypatch.setattr(module, "PROMPT_TIMEOUT_SECONDS", 0.15)
|
||||
monkeypatch.setattr(module, "REMOTE_TIMEOUT_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_KILL_AFTER_SECONDS", 1)
|
||||
monkeypatch.setattr(module, "REMOTE_TERMINATION_MARGIN_SECONDS", 0.1)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets, behavior="exit-after-first")
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] == "outcome_unknown"
|
||||
assert result["reason"] == "psql_result_uncertain"
|
||||
assert states[module.RUNTIME_ROLE] == "unknown"
|
||||
assert states[module.MIGRATOR_ROLE] == "pending"
|
||||
assert calls == [
|
||||
("intent", module.RUNTIME_ROLE),
|
||||
("unknown", module.RUNTIME_ROLE),
|
||||
]
|
||||
assert not marker.exists() or not any(
|
||||
value in marker.read_text(encoding="utf-8")
|
||||
for value in secrets.values()
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_fixed_uri_blocks_before_any_intent_or_process(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
monkeypatch.setattr(module, "read_database_uri", lambda *_args: None)
|
||||
secrets = _secrets(module)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] == "blocked"
|
||||
assert result["reason"] == "database_uri_unavailable"
|
||||
assert states == {module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
assert calls == []
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_concurrent_call_stops_on_durable_intent_without_starting_a_second_child(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
entered = threading.Event()
|
||||
release = threading.Event()
|
||||
child_calls = []
|
||||
|
||||
def hold_child(role, _password):
|
||||
child_calls.append(role)
|
||||
entered.set()
|
||||
assert release.wait(timeout=2)
|
||||
return "failed", False
|
||||
|
||||
monkeypatch.setattr(module, "_run_password_command", hold_child)
|
||||
first_result = []
|
||||
|
||||
thread = threading.Thread(
|
||||
target=lambda: first_result.append(_run(module, tmp_path, journal)),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
assert entered.wait(timeout=1)
|
||||
|
||||
second_result = _run(module, tmp_path, journal)
|
||||
release.set()
|
||||
thread.join(timeout=2)
|
||||
|
||||
assert not thread.is_alive()
|
||||
assert second_result["status"] == "outcome_unknown"
|
||||
assert second_result["reason"] == "role_requires_login_reconciliation"
|
||||
assert first_result[0]["status"] == "failed"
|
||||
assert child_calls == [module.RUNTIME_ROLE]
|
||||
assert states[module.RUNTIME_ROLE] == "retryable"
|
||||
assert calls == [
|
||||
("intent", module.RUNTIME_ROLE),
|
||||
("retryable", module.RUNTIME_ROLE),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("claim_result", [False, None])
|
||||
def test_failed_durable_intent_claim_never_starts_child(
|
||||
credentials_module, monkeypatch, tmp_path, claim_result
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
marker = _install_fake_docker(module, tmp_path, secrets)
|
||||
states, calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
journal["persist_role_intent"] = lambda _role: claim_result
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] == "outcome_unknown"
|
||||
assert result["reason"] == "role_intent_persistence_failed"
|
||||
assert states[module.RUNTIME_ROLE] == "pending"
|
||||
assert calls == []
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_exited_child_prompt_output_is_drained_without_sending_password(
|
||||
credentials_module, monkeypatch, tmp_path
|
||||
):
|
||||
module = credentials_module
|
||||
secrets = _secrets(module)
|
||||
_install_uri_reader(monkeypatch, module, secrets)
|
||||
_install_fake_docker(module, tmp_path, secrets, behavior="prompts-then-exit")
|
||||
states, _calls, journal = _journal(
|
||||
{module.RUNTIME_ROLE: "pending", module.MIGRATOR_ROLE: "pending"}
|
||||
)
|
||||
child = []
|
||||
writes = []
|
||||
original_popen = module.subprocess.Popen
|
||||
original_select = module.select.select
|
||||
original_write_password = module._write_password_line
|
||||
|
||||
def capture_popen(*args, **kwargs):
|
||||
process = original_popen(*args, **kwargs)
|
||||
child.append(process)
|
||||
return process
|
||||
|
||||
def select_after_exit(*args, **kwargs):
|
||||
child[0].wait(timeout=2)
|
||||
return original_select(*args, **kwargs)
|
||||
|
||||
def record_write(*args, **kwargs):
|
||||
writes.append(True)
|
||||
return original_write_password(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(module.subprocess, "Popen", capture_popen)
|
||||
monkeypatch.setattr(module.select, "select", select_after_exit)
|
||||
monkeypatch.setattr(module, "_write_password_line", record_write)
|
||||
|
||||
result = _run(module, tmp_path, journal)
|
||||
|
||||
assert result["status"] != "completed"
|
||||
assert writes == []
|
||||
assert states[module.RUNTIME_ROLE] != "completed"
|
||||
assert states[module.RUNTIME_ROLE] == "retryable"
|
||||
|
||||
|
||||
def test_zero_length_pty_write_fails_closed(credentials_module, monkeypatch):
|
||||
module = credentials_module
|
||||
monkeypatch.setattr(module.os, "write", lambda *_args: 0)
|
||||
monkeypatch.setattr(
|
||||
module.select,
|
||||
"select",
|
||||
lambda _readers, writers, _errors, _timeout: ([], writers, []),
|
||||
)
|
||||
|
||||
with pytest.raises(OSError):
|
||||
module._write_password_line(123, bytearray(b"bounded-secret"))
|
||||
|
||||
|
||||
def test_pty_write_backpressure_expires_within_fixed_budget(
|
||||
credentials_module, monkeypatch
|
||||
):
|
||||
module = credentials_module
|
||||
ticks = iter(range(0, 10))
|
||||
monkeypatch.setattr(module.time, "monotonic", lambda: float(next(ticks)))
|
||||
monkeypatch.setattr(
|
||||
module.select,
|
||||
"select",
|
||||
lambda _readers, _writers, _errors, _timeout: ([], [], []),
|
||||
)
|
||||
monkeypatch.setattr(module, "PTY_WRITE_TIMEOUT_SECONDS", 2.0)
|
||||
|
||||
with pytest.raises(TimeoutError):
|
||||
module._write_password_line(123, bytearray(b"bounded-secret"))
|
||||
|
||||
|
||||
def test_uri_password_decoder_rejects_malformed_or_unbounded_values(credentials_module):
|
||||
module = credentials_module
|
||||
role = module.RUNTIME_ROLE
|
||||
|
||||
assert module._password_from_uri(_uri(role, "x" * 31), role) is None
|
||||
assert module._password_from_uri(_uri(role, "x" * 257), role) is None
|
||||
assert module._password_from_uri(
|
||||
f"postgresql://{role}:bad%Q0value-with-more-than-32-characters@postgres:5432/forge".encode(),
|
||||
role,
|
||||
) is None
|
||||
|
||||
decoded = module._password_from_uri(
|
||||
f"postgresql://{role}:encoded%2Dpassword%2D0123456789abcdef@postgres:5432/forge".encode(),
|
||||
role,
|
||||
)
|
||||
assert decoded == bytearray(b"encoded-password-0123456789abcdef")
|
||||
decoded[:] = b"\0" * len(decoded)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,300 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
|
||||
from backend.app.services import user_access
|
||||
|
||||
_REAL_CURRENT_USER = user_access.current_user
|
||||
_PROVISION_PATH = "/api/apps/authentik/forge/oidc/provision"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forge_identity_client(
|
||||
tmp_path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.db.init_db import init_db
|
||||
from backend.app.main import app
|
||||
from backend.app.services import forge_oidc_provisioning
|
||||
|
||||
if os.name == "nt":
|
||||
# The shared Windows runner temp ACL is deliberately not the production
|
||||
# data-directory ACL; isolate route tests from that host configuration.
|
||||
monkeypatch.setattr(
|
||||
forge_oidc_provisioning,
|
||||
"_windows_private_path",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
else:
|
||||
monkeypatch.setattr(
|
||||
forge_oidc_provisioning,
|
||||
"_FORGE_OIDC_LOCK_ROOT",
|
||||
tmp_path / "forge-oidc-runtime-lock",
|
||||
)
|
||||
|
||||
engine = create_engine(
|
||||
f"sqlite:///{tmp_path / 'forge-oidc-auth.db'}",
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
monkeypatch.setattr(db_session, "engine", engine)
|
||||
monkeypatch.setenv("APPOPS_REQUIRE_OPERATOR_AUTH", "false")
|
||||
monkeypatch.setattr(settings, "operator_proxy_auth_enabled", False)
|
||||
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"], raising=False)
|
||||
monkeypatch.setattr(settings, "auth_admin_emails", [], raising=False)
|
||||
monkeypatch.setattr(settings, "data_dir", str(tmp_path))
|
||||
monkeypatch.setattr(user_access, "current_user", _REAL_CURRENT_USER)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
init_db()
|
||||
client = TestClient(app, client=("198.18.10.20", 42000))
|
||||
yield client
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _sign_in(client: TestClient, subject: str) -> None:
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.services.user_access import issue_session, provision_user
|
||||
|
||||
with Session(db_session.engine) as session:
|
||||
user = provision_user(
|
||||
session,
|
||||
"authentik",
|
||||
"https://auth.example/application/o/appops/",
|
||||
{"sub": subject, "email": f"{subject}@example.com"},
|
||||
)
|
||||
cookie = issue_session(session, user)
|
||||
client.cookies.set("appops_user_session", cookie)
|
||||
|
||||
|
||||
def test_forge_oidc_route_requires_authenticated_operator_and_csrf(
|
||||
forge_identity_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.services import setup_service
|
||||
|
||||
original_loader = setup_service.apply_stored_connector_settings
|
||||
loader_calls = []
|
||||
|
||||
def tracked_loader(session, connector_ids=None):
|
||||
loader_calls.append(connector_ids)
|
||||
return original_loader(session, connector_ids)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked_loader)
|
||||
unauthenticated = forge_identity_client.post(_PROVISION_PATH, json={})
|
||||
assert unauthenticated.status_code == 401
|
||||
|
||||
_sign_in(forge_identity_client, "owner")
|
||||
missing_csrf = forge_identity_client.post(_PROVISION_PATH, json={})
|
||||
assert missing_csrf.status_code == 403
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_oidc_route_rejects_read_only_user_and_nonempty_body(
|
||||
forge_identity_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.services import setup_service
|
||||
|
||||
original_loader = setup_service.apply_stored_connector_settings
|
||||
loader_calls = []
|
||||
|
||||
def tracked_loader(session, connector_ids=None):
|
||||
loader_calls.append(connector_ids)
|
||||
return original_loader(session, connector_ids)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked_loader)
|
||||
_sign_in(forge_identity_client, "reader")
|
||||
denied = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert denied.status_code == 403
|
||||
|
||||
_sign_in(forge_identity_client, "owner")
|
||||
invalid = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={"client_id": "caller-selected"},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert invalid.status_code == 422
|
||||
missing_body = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert missing_body.status_code == 422
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_oidc_route_loads_stored_connector_settings_and_passes_actor(
|
||||
forge_identity_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.db.models import AppSetting
|
||||
from backend.app.services import forge_oidc_provisioning, setup_service
|
||||
from backend.app.services.secret_store import set_secret
|
||||
|
||||
_sign_in(forge_identity_client, "owner")
|
||||
loader_calls = []
|
||||
constructed = []
|
||||
outcome_identities = []
|
||||
events = []
|
||||
protected_admin_token = "protected-authentik-admin-token-fixture"
|
||||
monkeypatch.setattr(settings, "environment", "production")
|
||||
monkeypatch.setattr(settings, "secret_key", "forge-oidc-api-secret-key-material-0123456789")
|
||||
monkeypatch.setattr(settings, "secret_key_source", "env")
|
||||
with Session(db_session.engine) as session:
|
||||
session.add(
|
||||
AppSetting(
|
||||
key="connector:authentik:config",
|
||||
value_json={"url": "https://auth.example", "verifyTls": True},
|
||||
is_secret=False,
|
||||
)
|
||||
)
|
||||
set_secret(session, "AUTHENTIK_TOKEN", protected_admin_token)
|
||||
session.commit()
|
||||
|
||||
original_loader = setup_service.apply_stored_connector_settings
|
||||
original_lock = forge_oidc_provisioning._forge_oidc_process_lock
|
||||
original_store_status = forge_oidc_provisioning.secret_storage_status
|
||||
|
||||
@contextmanager
|
||||
def tracked_lock():
|
||||
with original_lock():
|
||||
events.append("lock-enter")
|
||||
yield
|
||||
events.append("lock-exit")
|
||||
|
||||
def tracked_store_status():
|
||||
events.append("store-check")
|
||||
return original_store_status()
|
||||
|
||||
def tracked_loader(session, connector_ids=None):
|
||||
events.append("connector-loader")
|
||||
loader_calls.append(connector_ids)
|
||||
assert connector_ids == {"authentik"}
|
||||
return original_loader(session, connector_ids)
|
||||
|
||||
class FakeAuthentikConnector:
|
||||
def __init__(self):
|
||||
constructed.append(
|
||||
(
|
||||
settings.apis.get("AUTHENTIK_URL"),
|
||||
settings.apis.get("AUTHENTIK_VERIFY_TLS"),
|
||||
settings.apis.get("AUTHENTIK_TOKEN"),
|
||||
)
|
||||
)
|
||||
|
||||
def ensure_forge_oidc_prerequisites(self, *, expected_client_secret):
|
||||
assert expected_client_secret is None
|
||||
return {"success": False, "status": "failed"}
|
||||
|
||||
original_outcome = forge_oidc_provisioning._connector_outcome
|
||||
|
||||
def tracked_outcome(session, *, identity, outcome, secret_created):
|
||||
outcome_identities.append(identity)
|
||||
return original_outcome(
|
||||
session,
|
||||
identity=identity,
|
||||
outcome=outcome,
|
||||
secret_created=secret_created,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked_loader)
|
||||
monkeypatch.setattr(forge_oidc_provisioning, "_forge_oidc_process_lock", tracked_lock)
|
||||
monkeypatch.setattr(forge_oidc_provisioning, "secret_storage_status", tracked_store_status)
|
||||
monkeypatch.setattr(forge_oidc_provisioning, "AuthentikConnector", FakeAuthentikConnector)
|
||||
monkeypatch.setattr(forge_oidc_provisioning, "_connector_outcome", tracked_outcome)
|
||||
response = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "failed"
|
||||
assert response.json()["message"] == "forge_oidc_provisioning_failed"
|
||||
assert constructed == [("https://auth.example", True, protected_admin_token)]
|
||||
assert loader_calls == [{"authentik"}]
|
||||
assert events == ["lock-enter", "store-check", "connector-loader", "lock-exit"]
|
||||
assert outcome_identities == ["authentik:owner"]
|
||||
assert protected_admin_token not in response.text
|
||||
assert "client_secret" not in response.json()
|
||||
|
||||
|
||||
def test_forge_oidc_production_store_not_ready_does_not_load_connector(
|
||||
forge_identity_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.services import setup_service
|
||||
|
||||
_sign_in(forge_identity_client, "owner")
|
||||
monkeypatch.setattr(settings, "environment", "production")
|
||||
monkeypatch.setattr(settings, "secret_key", "")
|
||||
monkeypatch.setattr(settings, "secret_key_source", "missing")
|
||||
loader_calls = []
|
||||
original_loader = setup_service.apply_stored_connector_settings
|
||||
|
||||
def tracked_loader(session, connector_ids=None):
|
||||
loader_calls.append(connector_ids)
|
||||
return original_loader(session, connector_ids)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked_loader)
|
||||
response = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "blocked"
|
||||
assert response.json()["message"] == "forge_oidc_secret_store_not_production_ready"
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_oidc_route_returns_blocked_when_exact_policy_is_disabled(
|
||||
forge_identity_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.db.models import Policy
|
||||
from backend.app.services import setup_service
|
||||
|
||||
loader_calls = []
|
||||
original_loader = setup_service.apply_stored_connector_settings
|
||||
|
||||
def tracked_loader(session, connector_ids=None):
|
||||
loader_calls.append(connector_ids)
|
||||
return original_loader(session, connector_ids)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked_loader)
|
||||
_sign_in(forge_identity_client, "owner")
|
||||
with Session(db_session.engine) as session:
|
||||
policy = session.exec(
|
||||
select(Policy).where(
|
||||
(Policy.action == "forge.oidc.provision")
|
||||
& (Policy.target == "itworx-forge")
|
||||
)
|
||||
).one()
|
||||
policy.manual = "blocked"
|
||||
session.add(policy)
|
||||
session.commit()
|
||||
|
||||
response = forge_identity_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "blocked"
|
||||
assert response.json()["message"] == "forge_oidc_exact_target_policy_required"
|
||||
assert loader_calls == []
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
|
||||
from backend.app.services import user_access
|
||||
|
||||
_REAL_CURRENT_USER = user_access.current_user
|
||||
_PROVISION_PATH = "/api/apps/authentik/forge/controller-reader/provision"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forge_reader_operator_client(tmp_path, monkeypatch: pytest.MonkeyPatch):
|
||||
from backend.app.core.config import settings
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.db.init_db import init_db
|
||||
from backend.app.main import app
|
||||
|
||||
if os.name == "nt":
|
||||
from backend.app.services import forge_oidc_provisioning
|
||||
|
||||
monkeypatch.setattr(
|
||||
forge_oidc_provisioning,
|
||||
"_windows_private_path",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
else:
|
||||
from backend.app.services import forge_oidc_provisioning
|
||||
|
||||
monkeypatch.setattr(
|
||||
forge_oidc_provisioning,
|
||||
"_FORGE_OIDC_LOCK_ROOT",
|
||||
tmp_path / "forge-reader-api-lock",
|
||||
)
|
||||
|
||||
engine = create_engine(
|
||||
f"sqlite:///{tmp_path / 'forge-reader-operator.db'}",
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
monkeypatch.setattr(db_session, "engine", engine)
|
||||
monkeypatch.setenv("APPOPS_REQUIRE_OPERATOR_AUTH", "false")
|
||||
monkeypatch.setattr(settings, "operator_proxy_auth_enabled", False)
|
||||
monkeypatch.setattr(settings, "auth_admin_subjects", ["authentik:owner"], raising=False)
|
||||
monkeypatch.setattr(settings, "auth_admin_emails", [], raising=False)
|
||||
monkeypatch.setattr(settings, "data_dir", str(tmp_path))
|
||||
monkeypatch.setattr(user_access, "current_user", _REAL_CURRENT_USER)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
init_db()
|
||||
client = TestClient(app, client=("198.18.10.20", 42000))
|
||||
yield client
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _spy_actual_connector_loader(monkeypatch: pytest.MonkeyPatch):
|
||||
from backend.app.services import setup_service
|
||||
|
||||
original = setup_service.apply_stored_connector_settings
|
||||
calls = []
|
||||
|
||||
def tracked(session, connector_ids=None):
|
||||
calls.append(connector_ids)
|
||||
return original(session, connector_ids)
|
||||
|
||||
monkeypatch.setattr(setup_service, "apply_stored_connector_settings", tracked)
|
||||
return calls
|
||||
|
||||
|
||||
def _sign_in(client: TestClient, subject: str) -> None:
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.services.user_access import issue_session, provision_user
|
||||
|
||||
with Session(db_session.engine) as session:
|
||||
user = provision_user(
|
||||
session,
|
||||
"authentik",
|
||||
"https://auth.example/application/o/appops/",
|
||||
{"sub": subject, "email": f"{subject}@example.com"},
|
||||
)
|
||||
cookie = issue_session(session, user)
|
||||
client.cookies.set("appops_user_session", cookie)
|
||||
|
||||
|
||||
def test_forge_reader_route_requires_authenticated_operator_and_csrf(
|
||||
forge_reader_operator_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
loader_calls = _spy_actual_connector_loader(monkeypatch)
|
||||
unauthenticated = forge_reader_operator_client.post(_PROVISION_PATH, json={})
|
||||
assert unauthenticated.status_code == 401
|
||||
|
||||
_sign_in(forge_reader_operator_client, "owner")
|
||||
missing_csrf = forge_reader_operator_client.post(_PROVISION_PATH, json={})
|
||||
assert missing_csrf.status_code == 403
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_reader_route_rejects_read_only_user_and_caller_selectors(
|
||||
forge_reader_operator_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
loader_calls = _spy_actual_connector_loader(monkeypatch)
|
||||
_sign_in(forge_reader_operator_client, "reader")
|
||||
denied = forge_reader_operator_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert denied.status_code == 403
|
||||
|
||||
_sign_in(forge_reader_operator_client, "owner")
|
||||
invalid = forge_reader_operator_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={"token": "caller-selected"},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
assert invalid.status_code == 422
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_reader_default_denied_policy_does_not_load_connector_or_mutate(
|
||||
forge_reader_operator_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.db.models import Policy
|
||||
|
||||
loader_calls = _spy_actual_connector_loader(monkeypatch)
|
||||
_sign_in(forge_reader_operator_client, "owner")
|
||||
with Session(db_session.engine) as session:
|
||||
policy = session.get(Policy, "policy-forge-oidc-provision")
|
||||
policy.manual = "blocked"
|
||||
session.add(policy)
|
||||
session.commit()
|
||||
|
||||
response = forge_reader_operator_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "blocked"
|
||||
assert response.json()["message"] == "forge_reader_exact_target_policy_required"
|
||||
assert loader_calls == []
|
||||
|
||||
|
||||
def test_forge_reader_route_passes_operator_and_projects_only_safe_result(
|
||||
forge_reader_operator_client: TestClient,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from backend.app.api import routes_authentik
|
||||
|
||||
_sign_in(forge_reader_operator_client, "owner")
|
||||
loader_calls = _spy_actual_connector_loader(monkeypatch)
|
||||
calls = []
|
||||
held_token = "reader-token-must-never-be-returned"
|
||||
|
||||
def provision(session, *, actor_identity):
|
||||
calls.append((session, actor_identity))
|
||||
return {
|
||||
"success": True,
|
||||
"status": "ready",
|
||||
"message": "forge_reader_ready",
|
||||
"ready": True,
|
||||
"secretRef": "ITWORX_FORGE_AUTHENTIK_READ_TOKEN",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(routes_authentik, "provision_forge_controller_reader", provision)
|
||||
response = forge_reader_operator_client.post(
|
||||
_PROVISION_PATH,
|
||||
json={},
|
||||
headers={"X-AppOps-Session-CSRF": "1"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"success": True,
|
||||
"status": "ready",
|
||||
"message": "forge_reader_ready",
|
||||
"ready": True,
|
||||
"secretRef": "ITWORX_FORGE_AUTHENTIK_READ_TOKEN",
|
||||
}
|
||||
assert calls and calls[0][1] == "authentik:owner"
|
||||
assert loader_calls == []
|
||||
assert held_token not in response.text
|
||||
@@ -0,0 +1,747 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BROKER = ROOT / "ops" / "gitea-deploy-broker"
|
||||
if str(BROKER) not in sys.path:
|
||||
sys.path.insert(0, str(BROKER))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bundle_module():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"forge_secret_bundle_test",
|
||||
BROKER / "forge_secret_bundle.py",
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _config(**overrides: str) -> bytes:
|
||||
value = {
|
||||
"schema": "itworx.forge-first-install-config/v4",
|
||||
"issuer": "https://auth.example.test/application/o/forge/",
|
||||
}
|
||||
value.update(overrides)
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
||||
|
||||
|
||||
def _initialize_test_roots(roots) -> None:
|
||||
for path in dict.fromkeys(
|
||||
(roots.config_root, roots.state_parent, roots.backup_root)
|
||||
):
|
||||
path.mkdir(mode=0o700)
|
||||
path.chmod(0o700)
|
||||
|
||||
|
||||
def test_validator_must_return_literal_true_before_any_filesystem_effect(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
|
||||
for result in (False, None, 1):
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda _raw, result=result: result,
|
||||
roots=roots,
|
||||
)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
def raises_with_secret(_raw: bytes) -> bool:
|
||||
raise RuntimeError("validation failed: " + oidc.decode("ascii"))
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError) as raised:
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=raises_with_secret,
|
||||
roots=roots,
|
||||
)
|
||||
assert oidc.decode("ascii") not in str(raised.value)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
b'{"schema":"v4","schema":"v4"}',
|
||||
b'{ "schema":"v4"}',
|
||||
b'{"schema":NaN}',
|
||||
b"[]",
|
||||
b"\xff",
|
||||
],
|
||||
)
|
||||
def test_rejects_duplicate_invalid_noncanonical_or_nonobject_config_without_effects(
|
||||
bundle_module, tmp_path, config
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
config,
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_rejects_supplied_secret_embedded_in_config_before_validator_or_writes(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
called = False
|
||||
|
||||
def validate(_raw: bytes) -> bool:
|
||||
nonlocal called
|
||||
called = True
|
||||
return True
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(note=oidc.decode("ascii")),
|
||||
validate_config=validate,
|
||||
roots=roots,
|
||||
)
|
||||
assert called is False
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
def test_rejects_escaped_secret_value_without_calling_validator(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
secret = b'oidc-"quoted"-secret-123456789'
|
||||
encoded = _config(note=secret.decode("ascii"))
|
||||
called = False
|
||||
|
||||
def validate(_raw: bytes) -> bool:
|
||||
nonlocal called
|
||||
called = True
|
||||
return True
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
secret,
|
||||
b"authentik-token-sentinel-123456789",
|
||||
encoded,
|
||||
validate_config=validate,
|
||||
roots=roots,
|
||||
)
|
||||
assert called is False
|
||||
assert list(tmp_path.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_creates_fixed_files_and_backup_and_retry_only_verifies(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
def validate(raw):
|
||||
return raw == config
|
||||
|
||||
|
||||
result = bundle_module._prepare_with_roots(
|
||||
oidc, authentik, config, validate_config=validate, roots=roots
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
}
|
||||
assert oidc.decode("ascii") not in repr(result)
|
||||
assert authentik.decode("ascii") not in repr(result)
|
||||
assert sorted(path.name for path in roots.secrets_root.iterdir()) == sorted(
|
||||
f"{name}.secret"
|
||||
for name in (
|
||||
"postgres_bootstrap_password",
|
||||
"runtime_database_url",
|
||||
"migrator_database_url",
|
||||
"oidc_client_secret",
|
||||
"session_secret",
|
||||
"master_key",
|
||||
"authentik_read_token",
|
||||
)
|
||||
)
|
||||
assert (roots.secrets_root / "oidc_client_secret.secret").read_bytes() == oidc
|
||||
assert (
|
||||
roots.secrets_root / "authentik_read_token.secret"
|
||||
).read_bytes() == authentik
|
||||
master_key = (roots.secrets_root / "master_key.secret").read_bytes()
|
||||
assert len(master_key) == 32
|
||||
assert (roots.backup_root / "master_key.backup").read_bytes() == master_key
|
||||
|
||||
import urllib.parse
|
||||
|
||||
generated = {
|
||||
(roots.secrets_root / "postgres_bootstrap_password.secret").read_bytes(),
|
||||
urllib.parse.urlsplit(
|
||||
(roots.secrets_root / "runtime_database_url.secret").read_text(
|
||||
encoding="ascii"
|
||||
)
|
||||
).password.encode("ascii"),
|
||||
urllib.parse.urlsplit(
|
||||
(roots.secrets_root / "migrator_database_url.secret").read_text(
|
||||
encoding="ascii"
|
||||
)
|
||||
).password.encode("ascii"),
|
||||
(roots.secrets_root / "session_secret.secret").read_bytes(),
|
||||
}
|
||||
assert len(generated) == 4
|
||||
assert all(len(value) >= 32 for value in generated)
|
||||
|
||||
from forge_first_install import FORGE_SECRETS
|
||||
|
||||
for name, (_logical, uid, gid, minimum, maximum) in FORGE_SECRETS.items():
|
||||
path = roots.secrets_root / f"{name}.secret"
|
||||
info = path.stat()
|
||||
assert (info.st_uid, info.st_gid) == (uid, gid)
|
||||
assert info.st_mode & 0o777 == 0o400
|
||||
assert minimum <= info.st_size <= maximum
|
||||
assert (roots.secrets_root.stat().st_mode & 0o777) == 0o700
|
||||
assert (roots.config_path.stat().st_mode & 0o777) == 0o600
|
||||
assert (roots.backup_root / "master_key.backup").stat().st_mode & 0o777 == 0o400
|
||||
|
||||
journal = roots.journal_path.read_text(encoding="utf-8")
|
||||
assert "oidc-secret-sentinel-123456789" not in journal
|
||||
assert "authentik-token-sentinel-123456789" not in journal
|
||||
assert "sha256" not in journal.lower()
|
||||
|
||||
before = {
|
||||
path.name: (path.stat().st_ino, path.read_bytes())
|
||||
for path in roots.secrets_root.iterdir()
|
||||
}
|
||||
backup_identity = (roots.backup_root / "master_key.backup").stat().st_ino
|
||||
config_identity = roots.config_path.stat().st_ino
|
||||
original_write = bundle_module._write_all
|
||||
|
||||
def reject_write(_fd: int, _payload: bytes) -> None:
|
||||
raise AssertionError("a completed retry must not write")
|
||||
|
||||
bundle_module._write_all = reject_write
|
||||
retry = bundle_module._prepare_with_roots(
|
||||
oidc, authentik, config, validate_config=validate, roots=roots
|
||||
)
|
||||
bundle_module._write_all = original_write
|
||||
assert retry == result
|
||||
assert before == {
|
||||
path.name: (path.stat().st_ino, path.read_bytes())
|
||||
for path in roots.secrets_root.iterdir()
|
||||
}
|
||||
assert (roots.backup_root / "master_key.backup").stat().st_ino == backup_identity
|
||||
assert roots.config_path.stat().st_ino == config_identity
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_partial_retry_reuses_already_committed_password_and_recovers_backup(
|
||||
bundle_module, monkeypatch, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
original_write = bundle_module._write_all
|
||||
failed = False
|
||||
|
||||
def fail_runtime_write(fd: int, payload: bytes) -> None:
|
||||
nonlocal failed
|
||||
if payload.startswith(b"postgresql://") and not failed:
|
||||
failed = True
|
||||
raise OSError("injected write failure")
|
||||
original_write(fd, payload)
|
||||
|
||||
monkeypatch.setattr(bundle_module, "_write_all", fail_runtime_write)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
bootstrap = roots.secrets_root / "postgres_bootstrap_password.secret"
|
||||
original_bootstrap = bootstrap.read_bytes()
|
||||
original_inode = bootstrap.stat().st_ino
|
||||
|
||||
monkeypatch.setattr(bundle_module, "_write_all", original_write)
|
||||
result = bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
assert result["status"] == "bundle_ready"
|
||||
assert bootstrap.read_bytes() == original_bootstrap
|
||||
assert bootstrap.stat().st_ino == original_inode
|
||||
master = (roots.secrets_root / "master_key.secret").read_bytes()
|
||||
assert (roots.backup_root / "master_key.backup").read_bytes() == master
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_link_failure_after_durable_stage_keeps_and_reuses_original_secret(
|
||||
bundle_module, monkeypatch, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
original_link = os.link
|
||||
failed = False
|
||||
|
||||
def fail_runtime_link(source, destination, **kwargs):
|
||||
nonlocal failed
|
||||
if destination == "runtime_database_url.secret" and not failed:
|
||||
failed = True
|
||||
raise OSError("injected link failure")
|
||||
return original_link(source, destination, **kwargs)
|
||||
|
||||
monkeypatch.setattr(os, "link", fail_runtime_link)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
|
||||
journal = json.loads(roots.journal_path.read_bytes())
|
||||
stage = journal["staging"]
|
||||
assert stage["kind"] == "secret"
|
||||
assert stage["name"] == "runtime_database_url"
|
||||
staged_file = roots.secrets_root / stage["temporary"]
|
||||
assert staged_file.is_file()
|
||||
original_payload = staged_file.read_bytes()
|
||||
assert original_payload.startswith(b"postgresql://forge_runtime:")
|
||||
|
||||
monkeypatch.setattr(os, "link", original_link)
|
||||
result = bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
|
||||
assert result["status"] == "bundle_ready"
|
||||
runtime_password = (roots.secrets_root / "runtime_database_url.secret").read_bytes()
|
||||
assert runtime_password == original_payload
|
||||
assert not staged_file.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_journal_directory_fsync_uncertainty_preserves_referenced_stage(
|
||||
bundle_module, monkeypatch, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
original_fsync = os.fsync
|
||||
failed = False
|
||||
|
||||
def fail_stage_directory_fsync(fd: int) -> None:
|
||||
nonlocal failed
|
||||
if not failed and stat.S_ISDIR(os.fstat(fd).st_mode):
|
||||
try:
|
||||
journal = json.loads(roots.journal_path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
journal = {}
|
||||
stage = journal.get("staging")
|
||||
if stage is not None and stage.get("name") == "runtime_database_url":
|
||||
failed = True
|
||||
raise OSError("injected directory fsync uncertainty")
|
||||
original_fsync(fd)
|
||||
|
||||
monkeypatch.setattr(os, "fsync", fail_stage_directory_fsync)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
|
||||
journal = json.loads(roots.journal_path.read_bytes())
|
||||
stage = journal["staging"]
|
||||
assert stage["name"] == "runtime_database_url"
|
||||
staged_file = roots.secrets_root / stage["temporary"]
|
||||
assert staged_file.is_file()
|
||||
original_payload = staged_file.read_bytes()
|
||||
|
||||
monkeypatch.setattr(os, "fsync", original_fsync)
|
||||
result = bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
assert result["status"] == "bundle_ready"
|
||||
assert (
|
||||
roots.secrets_root / "runtime_database_url.secret"
|
||||
).read_bytes() == original_payload
|
||||
assert not staged_file.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_backup_link_failure_reuses_same_master_key_and_completes_backup(
|
||||
bundle_module, monkeypatch, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
oidc = b"oidc-secret-sentinel-123456789"
|
||||
authentik = b"authentik-token-sentinel-123456789"
|
||||
config = _config()
|
||||
original_link = os.link
|
||||
failed = False
|
||||
|
||||
def fail_backup_link(source, destination, **kwargs):
|
||||
nonlocal failed
|
||||
if destination == "master_key.backup" and not failed:
|
||||
failed = True
|
||||
raise OSError("injected backup link failure")
|
||||
return original_link(source, destination, **kwargs)
|
||||
|
||||
monkeypatch.setattr(os, "link", fail_backup_link)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
|
||||
journal = json.loads(roots.journal_path.read_bytes())
|
||||
stage = journal["staging"]
|
||||
assert stage["kind"] == "backup"
|
||||
assert stage["name"] == "master_key.backup"
|
||||
staged_file = roots.backup_root / stage["temporary"]
|
||||
original_payload = staged_file.read_bytes()
|
||||
original_master = (roots.secrets_root / "master_key.secret").read_bytes()
|
||||
original_master_inode = (roots.secrets_root / "master_key.secret").stat().st_ino
|
||||
assert original_payload == original_master
|
||||
assert len(original_payload) == 32
|
||||
|
||||
monkeypatch.setattr(os, "link", original_link)
|
||||
result = bundle_module._prepare_with_roots(
|
||||
oidc,
|
||||
authentik,
|
||||
config,
|
||||
validate_config=lambda raw: raw == config,
|
||||
roots=roots,
|
||||
)
|
||||
|
||||
assert result["status"] == "bundle_ready"
|
||||
assert (roots.secrets_root / "master_key.secret").read_bytes() == original_master
|
||||
assert (
|
||||
roots.secrets_root / "master_key.secret"
|
||||
).stat().st_ino == original_master_inode
|
||||
assert (roots.backup_root / "master_key.backup").read_bytes() == original_payload
|
||||
assert not staged_file.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_existing_secret_residue_is_never_overwritten_or_adopted(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
roots.secrets_root.mkdir(mode=0o700)
|
||||
residue = roots.secrets_root / "session_secret.secret"
|
||||
residue.write_bytes(b"foreign-residue-must-survive")
|
||||
residue.chmod(0o400)
|
||||
before = residue.read_bytes()
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert residue.read_bytes() == before
|
||||
assert not roots.journal_path.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_unsafe_ancestor_and_symlinked_secret_root_fail_without_chmod(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
roots.config_root.chmod(0o777)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert roots.config_root.stat().st_mode & 0o777 == 0o777
|
||||
|
||||
roots.config_root.chmod(0o700)
|
||||
target = tmp_path / "foreign-secret-dir"
|
||||
target.mkdir(mode=0o700)
|
||||
roots.secrets_root.symlink_to(target, target_is_directory=True)
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert list(target.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_stale_journal_generation_is_rejected_by_compare_and_swap(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
roots.state_root.mkdir(mode=0o700)
|
||||
descriptor = os.open(roots.state_root, os.O_RDONLY | os.O_DIRECTORY)
|
||||
try:
|
||||
record = bundle_module._empty_journal()
|
||||
bundle_module._write_journal(descriptor, record, expected_generation=None)
|
||||
stale = dict(record)
|
||||
record["status"] = "progress"
|
||||
bundle_module._persist(descriptor, record)
|
||||
before = roots.journal_path.read_bytes()
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._write_journal(descriptor, stale, expected_generation=0)
|
||||
assert roots.journal_path.read_bytes() == before
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_backup_root_rejects_untracked_files_before_creating_journal(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
foreign = roots.backup_root / "unrelated-backup-residue"
|
||||
foreign.write_bytes(b"leave this untouched")
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert foreign.read_bytes() == b"leave this untouched"
|
||||
assert not roots.state_root.exists()
|
||||
assert not roots.secrets_root.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
@pytest.mark.parametrize("target", ["config", "backup"])
|
||||
def test_existing_config_or_backup_files_are_never_overwritten(
|
||||
bundle_module, tmp_path, target
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
path = (
|
||||
roots.config_path
|
||||
if target == "config"
|
||||
else roots.backup_root / "master_key.backup"
|
||||
)
|
||||
path.write_bytes(b"foreign-config-or-backup")
|
||||
path.chmod(0o600)
|
||||
original = path.read_bytes()
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert path.read_bytes() == original
|
||||
assert not roots.state_root.exists()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_hardlinked_existing_secret_fails_closed_without_rekeying(
|
||||
bundle_module, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
args = (
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
)
|
||||
bundle_module._prepare_with_roots(
|
||||
*args,
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
master = roots.secrets_root / "master_key.secret"
|
||||
original = master.read_bytes()
|
||||
foreign_link = tmp_path / "foreign-master-link"
|
||||
os.link(master, foreign_link)
|
||||
journal_before = roots.journal_path.read_bytes()
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
*args,
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert master.read_bytes() == original
|
||||
assert roots.journal_path.read_bytes() == journal_before
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_pinned_file_reader_rejects_symlink_swap_between_stat_and_open(
|
||||
bundle_module, monkeypatch, tmp_path
|
||||
):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
roots.secrets_root.mkdir(mode=0o700)
|
||||
target = roots.secrets_root / "trusted-content"
|
||||
target.write_bytes(b"trusted")
|
||||
target.chmod(0o400)
|
||||
race = roots.secrets_root / "race-target"
|
||||
race.write_bytes(b"before-race")
|
||||
race.chmod(0o400)
|
||||
directory_fd = os.open(roots.secrets_root, os.O_RDONLY | os.O_DIRECTORY)
|
||||
original_stat_at = bundle_module._stat_at
|
||||
swapped = False
|
||||
|
||||
def swap_after_stat(fd: int, name: str):
|
||||
nonlocal swapped
|
||||
info = original_stat_at(fd, name)
|
||||
if name == "race-target" and info is not None and not swapped:
|
||||
swapped = True
|
||||
os.unlink(name, dir_fd=fd)
|
||||
os.symlink("trusted-content", name, dir_fd=fd)
|
||||
return info
|
||||
|
||||
monkeypatch.setattr(bundle_module, "_stat_at", swap_after_stat)
|
||||
try:
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._read_file_at(
|
||||
directory_fd,
|
||||
"race-target",
|
||||
uid=0,
|
||||
gid=0,
|
||||
min_size=1,
|
||||
max_size=32,
|
||||
allowed_modes=frozenset({0o400}),
|
||||
)
|
||||
finally:
|
||||
os.close(directory_fd)
|
||||
assert swapped is True
|
||||
assert race.is_symlink()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the production helper requires POSIX root ownership",
|
||||
)
|
||||
def test_changed_master_key_backup_is_not_silently_rewritten(bundle_module, tmp_path):
|
||||
roots = bundle_module._Roots.for_test(tmp_path)
|
||||
_initialize_test_roots(roots)
|
||||
args = (
|
||||
b"oidc-secret-sentinel-123456789",
|
||||
b"authentik-token-sentinel-123456789",
|
||||
_config(),
|
||||
)
|
||||
bundle_module._prepare_with_roots(
|
||||
*args,
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
backup = roots.backup_root / "master_key.backup"
|
||||
original = backup.read_bytes()
|
||||
backup.write_bytes(bytes(byte ^ 0xFF for byte in original))
|
||||
journal_before = roots.journal_path.read_bytes()
|
||||
|
||||
with pytest.raises(bundle_module.ForgeSecretBundleError):
|
||||
bundle_module._prepare_with_roots(
|
||||
*args,
|
||||
validate_config=lambda _raw: True,
|
||||
roots=roots,
|
||||
)
|
||||
assert backup.read_bytes() == bytes(byte ^ 0xFF for byte in original)
|
||||
assert (roots.secrets_root / "master_key.secret").read_bytes() == original
|
||||
assert roots.journal_path.read_bytes() == journal_before
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,863 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import http.client
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import types
|
||||
import uuid
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
BROKER = ROOT / "ops" / "gitea-deploy-broker"
|
||||
KEY = b"forge-delivery-test-hmac-key-32bytes"
|
||||
OIDC_SECRET = b"oidc-client-secret-sentinel-1234567890"
|
||||
READ_TOKEN = b"authentik-read-token-sentinel-1234567890"
|
||||
ARTIFACT = {
|
||||
"controllerVersion": "1.2.3",
|
||||
"sourceRepository": "jens/unraid-appops-gateway",
|
||||
"sourceRevision": "1" * 40,
|
||||
"sourceTree": "2" * 40,
|
||||
"artifactSha256": "3" * 64,
|
||||
"builtAt": "2026-10-03T10:00:00Z",
|
||||
}
|
||||
PROFILE = {
|
||||
"api_version": "itworx.forge-bootstrap-preflight/v1",
|
||||
"revision": "a" * 40,
|
||||
"workspace_image_digest": "sha256:" + "b" * 64,
|
||||
"frontdoor_image_digest": "sha256:" + "c" * 64,
|
||||
}
|
||||
|
||||
|
||||
def _load_status_module():
|
||||
try:
|
||||
import fcntl # noqa: F401 - use the real Linux flock implementation in tests.
|
||||
except ImportError: # pragma: no cover - Windows-only import shim.
|
||||
fcntl = types.ModuleType("fcntl")
|
||||
fcntl.LOCK_EX = 2
|
||||
fcntl.LOCK_UN = 8
|
||||
fcntl.flock = lambda *_args: None
|
||||
sys.modules["fcntl"] = fcntl
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"forge_secret_delivery_status_test", BROKER / "status_server.py"
|
||||
)
|
||||
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 status_module():
|
||||
return _load_status_module()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def endpoint(status_module):
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), status_module.Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
yield server.server_address
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=2)
|
||||
|
||||
|
||||
def _config(**overrides: str) -> bytes:
|
||||
value = {
|
||||
"schema": "itworx.forge-first-install-config/v4",
|
||||
"issuer": "https://auth.example-host.com/application/o/itworx-forge/",
|
||||
"audience": "itworx-forge",
|
||||
"client_id": "itworx-forge",
|
||||
"authentik_api_url": "https://auth.example-host.com",
|
||||
"launch_url": "https://forge.example.com",
|
||||
"authorization_flow": "default-provider-authorization-implicit-consent",
|
||||
"invalidation_flow": "default-provider-invalidation-flow",
|
||||
"authentik_openid_mapping_id": "01234567-89ab-4cde-8fab-0123456789ab",
|
||||
"authentik_email_mapping_id": "21234567-89ab-4cde-8fab-0123456789ab",
|
||||
"authentik_profile_mapping_id": "31234567-89ab-4cde-8fab-0123456789ab",
|
||||
"authentik_provider_id": "1234567890",
|
||||
"authentik_signing_key_id": "11234567-89ab-4cde-8fab-0123456789ab",
|
||||
"private_package_owner": "itworx-forge-private",
|
||||
}
|
||||
value.update(overrides)
|
||||
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
|
||||
|
||||
|
||||
def _payload(status_module, *, config_bytes: bytes | None = None, **overrides):
|
||||
config_bytes = config_bytes if config_bytes is not None else _config()
|
||||
value = {
|
||||
"apiVersion": status_module.FORGE_SECRET_DELIVERY_API_VERSION,
|
||||
"target": status_module.FORGE_SECRET_DELIVERY_TARGET,
|
||||
"profile": dict(PROFILE),
|
||||
"configBase64": base64.b64encode(config_bytes).decode("ascii"),
|
||||
"oidcClientSecretBase64": base64.b64encode(OIDC_SECRET).decode("ascii"),
|
||||
"authentikReadTokenBase64": base64.b64encode(READ_TOKEN).decode("ascii"),
|
||||
}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
def _signed_headers(status_module, body: bytes, *, nonce: str | None = None):
|
||||
timestamp = str(int(time.time()))
|
||||
nonce = nonce or uuid.uuid4().hex
|
||||
signature = status_module._request_signature(
|
||||
KEY,
|
||||
method="POST",
|
||||
path=status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce,
|
||||
raw_body=body,
|
||||
)
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"X-ITWorx-Deployment-Timestamp": timestamp,
|
||||
"X-ITWorx-Deployment-Nonce": nonce,
|
||||
"X-ITWorx-Deployment-Signature": signature,
|
||||
}
|
||||
|
||||
|
||||
def _post(address, path: str, body: bytes, headers: dict[str, str]):
|
||||
connection = http.client.HTTPConnection(*address, timeout=3)
|
||||
try:
|
||||
connection.request("POST", path, body=body, headers=headers)
|
||||
response = connection.getresponse()
|
||||
return response.status, response.read()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
|
||||
def _install_hmac_test(monkeypatch, status_module):
|
||||
consumed: set[str] = set()
|
||||
monkeypatch.setattr(status_module, "_forge_first_install_secret", lambda: KEY)
|
||||
|
||||
def consume(_secret, nonce):
|
||||
if nonce in consumed:
|
||||
return False
|
||||
consumed.add(nonce)
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(status_module, "_consume_appops_nonce", consume)
|
||||
return consumed
|
||||
|
||||
|
||||
def _install_successful_prerequisites(monkeypatch, status_module, helper=None):
|
||||
counters = {"artifact": 0, "store": 0, "backup": 0, "runtime": 0}
|
||||
|
||||
def artifact():
|
||||
counters["artifact"] += 1
|
||||
return dict(ARTIFACT)
|
||||
|
||||
def store():
|
||||
counters["store"] += 1
|
||||
return ("/var/lib/docker", "itworx-controller-hub-releases", (1, 2), (1, 2))
|
||||
|
||||
def backup():
|
||||
counters["backup"] += 1
|
||||
return True
|
||||
|
||||
def runtime(profile, *, _held_candidate):
|
||||
counters["runtime"] += 1
|
||||
assert profile == PROFILE
|
||||
assert isinstance(_held_candidate, dict)
|
||||
return {"status": "bundle_prerequisites_passed"}
|
||||
|
||||
monkeypatch.setattr(status_module, "_forge_first_install_requests_are_absent", lambda: True)
|
||||
monkeypatch.setattr(status_module, "_forge_controller_artifact_identity", artifact)
|
||||
monkeypatch.setattr(status_module, "_forge_release_store_identity", store)
|
||||
monkeypatch.setattr(status_module, "_forge_fixed_backup_volume_is_ready", backup)
|
||||
monkeypatch.setattr(status_module, "_forge_bootstrap_runtime_checks", runtime)
|
||||
if helper is not None:
|
||||
monkeypatch.setattr(status_module, "_prepare_forge_secret_bundle", helper)
|
||||
return counters
|
||||
|
||||
|
||||
def test_candidate_parser_requires_canonical_config_and_secret_free_strings(status_module):
|
||||
parsed = status_module._forge_secret_delivery_candidate(_payload(status_module))
|
||||
assert parsed is not None
|
||||
profile, candidate = parsed
|
||||
assert profile == PROFILE
|
||||
assert candidate["config_bytes"] == _config()
|
||||
assert candidate["oidc_client_secret"] == OIDC_SECRET
|
||||
assert candidate["authentik_read_token"] == READ_TOKEN
|
||||
|
||||
pretty = json.dumps(json.loads(_config()), indent=2).encode()
|
||||
assert status_module._forge_secret_delivery_candidate(
|
||||
_payload(status_module, config_bytes=pretty)
|
||||
) is None
|
||||
secret_bearing_config = _config(authorization_flow=OIDC_SECRET.decode())
|
||||
assert status_module._forge_secret_delivery_candidate(
|
||||
_payload(status_module, config_bytes=secret_bearing_config)
|
||||
) is None
|
||||
noncanonical = _payload(status_module)
|
||||
noncanonical["oidcClientSecretBase64"] += "\n"
|
||||
assert status_module._forge_secret_delivery_candidate(noncanonical) is None
|
||||
|
||||
|
||||
def test_invalid_authentication_has_no_remote_or_bundle_side_effects(
|
||||
status_module, endpoint, monkeypatch
|
||||
):
|
||||
counters = {"runtime": 0, "bundle": 0}
|
||||
consumed = _install_hmac_test(monkeypatch, status_module)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_controller_artifact_identity",
|
||||
lambda: pytest.fail("unauthenticated input reached local gate"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_release_store_identity",
|
||||
lambda: pytest.fail("unauthenticated input reached volume probe"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_fixed_backup_volume_is_ready",
|
||||
lambda: pytest.fail("unauthenticated input reached backup probe"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_bootstrap_runtime_checks",
|
||||
lambda *_args, **_kwargs: counters.__setitem__("runtime", counters["runtime"] + 1),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_prepare_forge_secret_bundle",
|
||||
lambda *_args, **_kwargs: counters.__setitem__("bundle", counters["bundle"] + 1),
|
||||
)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
headers = _signed_headers(status_module, body)
|
||||
headers["X-ITWorx-Deployment-Signature"] = "0" * 64
|
||||
|
||||
status, response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
headers,
|
||||
)
|
||||
assert status == 403
|
||||
assert response == b'{"status":"authentication_rejected"}\n'
|
||||
assert counters == {"runtime": 0, "bundle": 0}
|
||||
assert consumed == set()
|
||||
|
||||
|
||||
def test_duplicate_json_key_is_rejected_before_auth_or_remote_checks(
|
||||
status_module, endpoint, monkeypatch
|
||||
):
|
||||
consumed = _install_hmac_test(monkeypatch, status_module)
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_prepare_secret_delivery_bundle",
|
||||
lambda *_args, **_kwargs: called.append(True),
|
||||
)
|
||||
body = b'{"apiVersion":"x","apiVersion":"y"}'
|
||||
status, response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
{
|
||||
"Content-Type": "application/json",
|
||||
"X-ITWorx-Deployment-Timestamp": str(int(time.time())),
|
||||
"X-ITWorx-Deployment-Nonce": uuid.uuid4().hex,
|
||||
"X-ITWorx-Deployment-Signature": "0" * 64,
|
||||
},
|
||||
)
|
||||
assert status == 400
|
||||
assert response == b'{"status":"invalid_json"}\n'
|
||||
assert called == []
|
||||
assert consumed == set()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("body", "content_type", "expected_status"),
|
||||
[
|
||||
(b"{}", "application/json; charset=utf-8", 400),
|
||||
(b"x" * 16_385, "application/json", 413),
|
||||
],
|
||||
)
|
||||
def test_body_limits_and_content_type_fail_before_auth(
|
||||
status_module, endpoint, monkeypatch, body, content_type, expected_status
|
||||
):
|
||||
consumed = _install_hmac_test(monkeypatch, status_module)
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"_forge_controller_artifact_identity",
|
||||
lambda: pytest.fail("invalid HTTP framing reached local gate"),
|
||||
)
|
||||
headers = {
|
||||
"Content-Type": content_type,
|
||||
"X-ITWorx-Deployment-Timestamp": str(int(time.time())),
|
||||
"X-ITWorx-Deployment-Nonce": uuid.uuid4().hex,
|
||||
"X-ITWorx-Deployment-Signature": "0" * 64,
|
||||
}
|
||||
status, _response = _post(
|
||||
endpoint, status_module.FORGE_SECRET_DELIVERY_PATH, body, headers
|
||||
)
|
||||
assert status == expected_status
|
||||
assert consumed == set()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failed_gate", ["artifact", "store", "backup", "runtime"])
|
||||
def test_every_fixed_prerequisite_gate_precedes_bundle_writer(
|
||||
status_module, endpoint, monkeypatch, failed_gate
|
||||
):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
calls = {name: 0 for name in ("artifact", "store", "backup", "runtime", "helper")}
|
||||
|
||||
def artifact():
|
||||
calls["artifact"] += 1
|
||||
return None if failed_gate == "artifact" else dict(ARTIFACT)
|
||||
|
||||
def store():
|
||||
calls["store"] += 1
|
||||
return None if failed_gate == "store" else ("fixed", "volume", (1, 2), (1, 2))
|
||||
|
||||
def backup():
|
||||
calls["backup"] += 1
|
||||
return failed_gate != "backup"
|
||||
|
||||
def runtime(profile, *, _held_candidate):
|
||||
calls["runtime"] += 1
|
||||
return {
|
||||
"status": "bundle_prerequisites_blocked"
|
||||
if failed_gate == "runtime"
|
||||
else "bundle_prerequisites_passed"
|
||||
}
|
||||
|
||||
def helper(*_args, **_kwargs):
|
||||
calls["helper"] += 1
|
||||
return {
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(status_module, "_forge_first_install_requests_are_absent", lambda: True)
|
||||
monkeypatch.setattr(status_module, "_forge_controller_artifact_identity", artifact)
|
||||
monkeypatch.setattr(status_module, "_forge_release_store_identity", store)
|
||||
monkeypatch.setattr(status_module, "_forge_fixed_backup_volume_is_ready", backup)
|
||||
monkeypatch.setattr(status_module, "_forge_bootstrap_runtime_checks", runtime)
|
||||
monkeypatch.setattr(status_module, "_prepare_forge_secret_bundle", helper)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
|
||||
status, _response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
assert status == 409
|
||||
assert calls["helper"] == 0
|
||||
if failed_gate in {"artifact", "store", "backup"}:
|
||||
assert calls["runtime"] == 0
|
||||
else:
|
||||
assert calls["runtime"] == 1
|
||||
|
||||
|
||||
def test_signed_route_returns_only_sanitized_bundle_readiness_and_replay_is_rejected(
|
||||
status_module, endpoint, monkeypatch
|
||||
):
|
||||
consumed = _install_hmac_test(monkeypatch, status_module)
|
||||
helper_calls = []
|
||||
|
||||
def helper(oidc_secret, read_token, config_bytes, *, validate_config):
|
||||
helper_calls.append((oidc_secret, read_token, config_bytes))
|
||||
assert oidc_secret == OIDC_SECRET
|
||||
assert read_token == READ_TOKEN
|
||||
assert validate_config(config_bytes) is True
|
||||
return {
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
}
|
||||
|
||||
counters = _install_successful_prerequisites(monkeypatch, status_module, helper)
|
||||
monkeypatch.setattr(
|
||||
status_module._forge_first_install_controller,
|
||||
"remember_preflight",
|
||||
lambda *_args: pytest.fail("bundle readiness must not remember a preflight"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module._forge_first_install_controller,
|
||||
"apply",
|
||||
lambda *_args: pytest.fail("bundle readiness must not start first install"),
|
||||
)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
headers = _signed_headers(status_module, body)
|
||||
nonce = headers["X-ITWorx-Deployment-Nonce"]
|
||||
status, response = _post(
|
||||
endpoint, status_module.FORGE_SECRET_DELIVERY_PATH, body, headers
|
||||
)
|
||||
assert status == 200
|
||||
decoded = json.loads(response)
|
||||
assert decoded == {
|
||||
"apiVersion": status_module.FORGE_SECRET_DELIVERY_API_VERSION,
|
||||
"target": "itworx-forge",
|
||||
"status": "bundle_ready",
|
||||
"profile": PROFILE,
|
||||
"controllerArtifact": {
|
||||
key: ARTIFACT[key]
|
||||
for key in (
|
||||
"controllerVersion",
|
||||
"sourceRepository",
|
||||
"sourceRevision",
|
||||
"sourceTree",
|
||||
"artifactSha256",
|
||||
)
|
||||
},
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
}
|
||||
assert "secretFilesReady" in decoded
|
||||
assert OIDC_SECRET.decode() not in response.decode()
|
||||
assert READ_TOKEN.decode() not in response.decode()
|
||||
assert base64.b64encode(OIDC_SECRET).decode() not in response.decode()
|
||||
assert base64.b64encode(READ_TOKEN).decode() not in response.decode()
|
||||
assert len(consumed) == 1 and nonce in consumed
|
||||
assert helper_calls == [(OIDC_SECRET, READ_TOKEN, _config())]
|
||||
assert counters == {"artifact": 2, "store": 2, "backup": 2, "runtime": 1}
|
||||
|
||||
status, replay_response = _post(
|
||||
endpoint, status_module.FORGE_SECRET_DELIVERY_PATH, body, headers
|
||||
)
|
||||
assert status == 403
|
||||
assert replay_response == b'{"status":"authentication_rejected"}\n'
|
||||
assert len(helper_calls) == 1
|
||||
assert counters == {"artifact": 2, "store": 2, "backup": 2, "runtime": 1}
|
||||
|
||||
|
||||
def test_new_nonce_retries_same_bundle_and_changed_input_is_rejected(
|
||||
status_module, endpoint, monkeypatch
|
||||
):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
from forge_secret_bundle import ForgeSecretBundleError
|
||||
|
||||
accepted_inputs = None
|
||||
helper_calls = []
|
||||
|
||||
def helper(oidc_secret, read_token, config_bytes, *, validate_config):
|
||||
nonlocal accepted_inputs
|
||||
inputs = (oidc_secret, read_token, config_bytes)
|
||||
helper_calls.append(inputs)
|
||||
assert validate_config(config_bytes) is True
|
||||
if accepted_inputs is None:
|
||||
accepted_inputs = inputs
|
||||
elif inputs != accepted_inputs:
|
||||
raise ForgeSecretBundleError("input_changed")
|
||||
return {
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
}
|
||||
|
||||
_install_successful_prerequisites(monkeypatch, status_module, helper)
|
||||
|
||||
def send(payload):
|
||||
body = json.dumps(payload, separators=(",", ":")).encode()
|
||||
return _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
|
||||
assert send(_payload(status_module))[0] == 200
|
||||
assert send(_payload(status_module))[0] == 200
|
||||
changed = _payload(status_module)
|
||||
changed["oidcClientSecretBase64"] = base64.b64encode(b"x" * 40).decode("ascii")
|
||||
status, response = send(changed)
|
||||
assert status == 409
|
||||
assert json.loads(response) == {
|
||||
"status": "secret_bundle_failed",
|
||||
"code": "input_changed",
|
||||
}
|
||||
assert accepted_inputs == (OIDC_SECRET, READ_TOKEN, _config())
|
||||
assert helper_calls == [
|
||||
accepted_inputs,
|
||||
accepted_inputs,
|
||||
(b"x" * 40, READ_TOKEN, _config()),
|
||||
]
|
||||
assert OIDC_SECRET.decode() not in response.decode()
|
||||
assert READ_TOKEN.decode() not in response.decode()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
os.name != "posix" or getattr(os, "geteuid", lambda: 1)() != 0,
|
||||
reason="the real bundle helper requires POSIX root ownership",
|
||||
)
|
||||
def test_signed_http_delivery_uses_real_bundle_helper_and_never_rotates(
|
||||
status_module, endpoint, monkeypatch, tmp_path
|
||||
):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
import importlib
|
||||
|
||||
bundle_module = importlib.import_module("forge_secret_bundle")
|
||||
assert Path(bundle_module.__file__).resolve() == (
|
||||
BROKER / "forge_secret_bundle.py"
|
||||
).resolve()
|
||||
root = tmp_path / "real-bundle-root"
|
||||
root.mkdir(mode=0o700)
|
||||
root.chmod(0o700)
|
||||
roots = bundle_module._Roots.for_test(root)
|
||||
for path in (roots.config_root, roots.state_parent, roots.backup_root):
|
||||
path.mkdir(mode=0o700, exist_ok=True)
|
||||
path.chmod(0o700)
|
||||
monkeypatch.setattr(bundle_module, "_PRODUCTION_ROOTS", roots)
|
||||
|
||||
counters = _install_successful_prerequisites(
|
||||
monkeypatch, status_module
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module._forge_first_install_controller,
|
||||
"remember_preflight",
|
||||
lambda *_args: pytest.fail("bundle readiness must not persist a preflight"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
status_module._forge_first_install_controller,
|
||||
"apply",
|
||||
lambda *_args: pytest.fail("bundle readiness must not start first install"),
|
||||
)
|
||||
|
||||
def send(payload):
|
||||
body = json.dumps(payload, separators=(",", ":")).encode()
|
||||
return _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
|
||||
status, response = send(_payload(status_module))
|
||||
assert status == 200
|
||||
decoded = json.loads(response)
|
||||
assert decoded["status"] == "bundle_ready"
|
||||
assert decoded["secretFilesReady"] == 7
|
||||
assert decoded["masterKeyBackupVerified"] is True
|
||||
assert decoded["applyPreflight"] == "not_evaluated"
|
||||
assert OIDC_SECRET.decode() not in response.decode()
|
||||
assert READ_TOKEN.decode() not in response.decode()
|
||||
|
||||
secret_paths = sorted(roots.secrets_root.iterdir())
|
||||
assert {path.name for path in secret_paths} == {
|
||||
"postgres_bootstrap_password.secret",
|
||||
"runtime_database_url.secret",
|
||||
"migrator_database_url.secret",
|
||||
"oidc_client_secret.secret",
|
||||
"session_secret.secret",
|
||||
"master_key.secret",
|
||||
"authentik_read_token.secret",
|
||||
}
|
||||
assert (roots.secrets_root / "oidc_client_secret.secret").read_bytes() == OIDC_SECRET
|
||||
assert (roots.secrets_root / "authentik_read_token.secret").read_bytes() == READ_TOKEN
|
||||
assert roots.config_path.read_bytes() == _config()
|
||||
master_key = (roots.secrets_root / "master_key.secret").read_bytes()
|
||||
assert len(master_key) == 32
|
||||
backup_path = roots.backup_root / "master_key.backup"
|
||||
assert backup_path.read_bytes() == master_key
|
||||
|
||||
protected_paths = [*secret_paths, roots.config_path, backup_path]
|
||||
|
||||
def identities():
|
||||
return {
|
||||
path: (path.stat().st_ino, path.read_bytes())
|
||||
for path in protected_paths
|
||||
}
|
||||
|
||||
original = identities()
|
||||
status, retry_response = send(_payload(status_module))
|
||||
assert status == 200
|
||||
assert json.loads(retry_response)["status"] == "bundle_ready"
|
||||
assert identities() == original
|
||||
|
||||
changed = _payload(status_module)
|
||||
changed["authentikReadTokenBase64"] = base64.b64encode(
|
||||
b"different-authentik-read-token-value-12345"
|
||||
).decode("ascii")
|
||||
status, changed_response = send(changed)
|
||||
assert status == 409
|
||||
assert json.loads(changed_response) == {
|
||||
"status": "secret_bundle_failed",
|
||||
"code": "input_changed",
|
||||
}
|
||||
assert identities() == original
|
||||
assert OIDC_SECRET.decode() not in changed_response.decode()
|
||||
assert READ_TOKEN.decode() not in changed_response.decode()
|
||||
assert counters == {"artifact": 6, "store": 6, "backup": 6, "runtime": 3}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_result",
|
||||
[
|
||||
{
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": True,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": 1,
|
||||
"applyPreflight": "not_evaluated",
|
||||
},
|
||||
{
|
||||
"status": "bundle_ready",
|
||||
"secretFilesReady": 7,
|
||||
"masterKeyBackupVerified": True,
|
||||
"applyPreflight": "not_evaluated",
|
||||
"unexpected": "must be rejected",
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_malformed_core_helper_results_are_not_accepted(
|
||||
status_module, endpoint, monkeypatch, bad_result
|
||||
):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
_install_successful_prerequisites(
|
||||
monkeypatch, status_module, lambda *_args, **_kwargs: bad_result
|
||||
)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
status, response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
assert status == 409
|
||||
assert json.loads(response) == {
|
||||
"status": "secret_bundle_failed",
|
||||
"code": "bundle_result_unverified",
|
||||
}
|
||||
|
||||
|
||||
def test_unrecognized_core_error_code_is_replaced_with_fixed_code(
|
||||
status_module, endpoint, monkeypatch
|
||||
):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
from forge_secret_bundle import ForgeSecretBundleError
|
||||
|
||||
private_error = "unapproved-" + OIDC_SECRET.decode()
|
||||
|
||||
def helper(*_args, **_kwargs):
|
||||
raise ForgeSecretBundleError(private_error)
|
||||
|
||||
_install_successful_prerequisites(monkeypatch, status_module, helper)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
status, response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
assert status == 409
|
||||
assert json.loads(response) == {
|
||||
"status": "secret_bundle_failed",
|
||||
"code": "bundle_preparation_failed",
|
||||
}
|
||||
assert private_error.encode() not in response
|
||||
assert OIDC_SECRET.decode().encode() not in response
|
||||
|
||||
|
||||
def test_helper_error_text_is_never_returned(status_module, endpoint, monkeypatch):
|
||||
_install_hmac_test(monkeypatch, status_module)
|
||||
|
||||
def helper(*_args, **_kwargs):
|
||||
raise RuntimeError("disk failed: " + OIDC_SECRET.decode())
|
||||
|
||||
_install_successful_prerequisites(monkeypatch, status_module, helper)
|
||||
body = json.dumps(_payload(status_module), separators=(",", ":")).encode()
|
||||
status, response = _post(
|
||||
endpoint,
|
||||
status_module.FORGE_SECRET_DELIVERY_PATH,
|
||||
body,
|
||||
_signed_headers(status_module, body),
|
||||
)
|
||||
assert status == 409
|
||||
assert OIDC_SECRET.decode() not in response.decode()
|
||||
assert READ_TOKEN.decode() not in response.decode()
|
||||
assert b"disk failed" not in response
|
||||
|
||||
|
||||
def test_held_candidate_runtime_skips_destination_secrets_and_preflight_receipt(
|
||||
status_module, monkeypatch, tmp_path
|
||||
):
|
||||
parsed = status_module._forge_secret_delivery_candidate(_payload(status_module))
|
||||
assert parsed is not None
|
||||
profile, candidate = parsed
|
||||
app_root = tmp_path / "managed-appdata"
|
||||
postgres = app_root / "itworx-forge" / "postgres"
|
||||
postgres.mkdir(parents=True)
|
||||
postgres.chmod(0o700)
|
||||
os.chown(postgres, 70, 70)
|
||||
app_root.chmod(0o700)
|
||||
monkeypatch.setattr(status_module, "MANAGED_APPDATA_ROOT", app_root)
|
||||
monkeypatch.setattr(status_module, "_refresh_appdata_root_selection", lambda: app_root)
|
||||
monkeypatch.setattr(status_module, "appdata_root_is_safe", lambda _root: True)
|
||||
monkeypatch.setattr(status_module, "_forge_app_dir_metadata_is_safe", lambda _info: True)
|
||||
monkeypatch.setattr(status_module, "_forge_gitea_main_revision", lambda: profile["revision"])
|
||||
manifest = b"fixed compose bytes used by this unit test"
|
||||
monkeypatch.setattr(
|
||||
status_module,
|
||||
"FORGE_COMPOSE_SHA256",
|
||||
hashlib.sha256(manifest).hexdigest(),
|
||||
)
|
||||
calls = {"registry": 0, "oidc": 0, "secret_metadata": 0}
|
||||
|
||||
def registry(_profile, *, candidate_config):
|
||||
calls["registry"] += 1
|
||||
assert candidate_config is candidate["config"]
|
||||
return "passed", {"ownerVisibility": "private"}
|
||||
|
||||
def oidc(*, held_candidate):
|
||||
calls["oidc"] += 1
|
||||
assert held_candidate is candidate
|
||||
return "passed", {"clientSecretMatched": True}
|
||||
|
||||
def secret_metadata(*_args, **_kwargs):
|
||||
calls["secret_metadata"] += 1
|
||||
pytest.fail("held candidate must not read destination secret files")
|
||||
|
||||
monkeypatch.setattr(status_module, "_forge_image_registry_attestation", registry)
|
||||
monkeypatch.setattr(status_module, "_forge_oidc_attestation", oidc)
|
||||
monkeypatch.setattr(status_module, "_forge_secret_metadata", secret_metadata)
|
||||
monkeypatch.setattr(status_module, "_load_forge_bootstrap_config", lambda *_args: pytest.fail("held config fell back to disk"))
|
||||
monkeypatch.setattr(status_module, "_forge_push_validation_attestation", lambda _revision: ("passed", {"context": "fixed"}))
|
||||
monkeypatch.setattr(status_module, "_forge_local_image_present", lambda _image: None)
|
||||
monkeypatch.setattr(
|
||||
status_module._forge_first_install_controller,
|
||||
"remember_preflight",
|
||||
lambda *_args: pytest.fail("secret bundle readiness must not persist a preflight receipt"),
|
||||
)
|
||||
|
||||
def fake_run(arguments, **_kwargs):
|
||||
if arguments[:2] == ["git", "--git-dir"]:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout=manifest, stderr=b"")
|
||||
if arguments == ["docker", "network", "ls", "--format", "{{.Name}}"]:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout="itworx-mcp-hub-npm\n", stderr="")
|
||||
if arguments[:3] == ["docker", "network", "inspect"]:
|
||||
return subprocess.CompletedProcess(
|
||||
arguments,
|
||||
0,
|
||||
stdout=json.dumps({"Name": "itworx-mcp-hub-npm", "Driver": "bridge", "Scope": "local"}),
|
||||
stderr="",
|
||||
)
|
||||
if arguments == ["docker", "ps", "-a", "--format", "{{.Names}}"]:
|
||||
return subprocess.CompletedProcess(arguments, 0, stdout="", stderr="")
|
||||
pytest.fail(f"unexpected runtime command: {arguments}")
|
||||
|
||||
monkeypatch.setattr(status_module.subprocess, "run", fake_run)
|
||||
result = status_module._forge_bootstrap_runtime_checks(
|
||||
profile, _held_candidate=candidate
|
||||
)
|
||||
assert result["status"] == "bundle_prerequisites_passed"
|
||||
assert "secret_delivery" not in result["checks"]
|
||||
assert calls == {"registry": 1, "oidc": 1, "secret_metadata": 0}
|
||||
|
||||
|
||||
def test_release_store_proof_checks_fixed_volume_alias_and_pinned_identity(
|
||||
status_module, monkeypatch
|
||||
):
|
||||
release_store = status_module._mcp_hub_release_store
|
||||
document = {
|
||||
"Name": release_store.VOLUME_NAME,
|
||||
"Driver": "local",
|
||||
"Scope": "local",
|
||||
"Mountpoint": release_store.VOLUME_HOST_ROOT.as_posix(),
|
||||
"Options": None,
|
||||
"Labels": dict(release_store.EXPECTED_LABELS),
|
||||
}
|
||||
commands = []
|
||||
|
||||
def run_fixed(arguments, *, maximum_stdout, **_kwargs):
|
||||
commands.append((arguments, maximum_stdout))
|
||||
if arguments[:2] == ["docker", "info"]:
|
||||
return b"/var/lib/docker\n", b""
|
||||
if arguments[:3] == ["docker", "volume", "inspect"]:
|
||||
return json.dumps(document).encode(), b""
|
||||
pytest.fail(f"unexpected command {arguments}")
|
||||
|
||||
class _PinnedDirectory:
|
||||
def __init__(self, identity):
|
||||
self.value = identity
|
||||
|
||||
def identity(self):
|
||||
return self.value
|
||||
|
||||
class _Store:
|
||||
host = _PinnedDirectory((4, 5))
|
||||
logical = _PinnedDirectory((4, 5))
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return None
|
||||
|
||||
def assert_current(self):
|
||||
return None
|
||||
|
||||
opened = []
|
||||
|
||||
def open_fixed(metadata, docker_root_dir):
|
||||
opened.append((metadata, docker_root_dir))
|
||||
return _Store()
|
||||
|
||||
monkeypatch.setattr(status_module, "_forge_bounded_readonly_command", run_fixed)
|
||||
monkeypatch.setattr(status_module, "_repopulse_adoption_alias_mounts_ready", lambda: True)
|
||||
monkeypatch.setattr(
|
||||
status_module._repopulse_adoption_store,
|
||||
"capability_report",
|
||||
lambda: {"controllerAliasesReady": True},
|
||||
)
|
||||
monkeypatch.setattr(release_store, "open_fixed_release_store", open_fixed)
|
||||
identity = status_module._forge_release_store_identity()
|
||||
assert identity == (
|
||||
"/var/lib/docker",
|
||||
release_store.VOLUME_NAME,
|
||||
(4, 5),
|
||||
(4, 5),
|
||||
)
|
||||
assert opened == [(document, "/var/lib/docker")]
|
||||
assert [item[0] for item in commands] == [
|
||||
["docker", "info", "--format", "{{.DockerRootDir}}"],
|
||||
[
|
||||
"docker",
|
||||
"volume",
|
||||
"inspect",
|
||||
"--format",
|
||||
"{{json .}}",
|
||||
release_store.VOLUME_NAME,
|
||||
],
|
||||
]
|
||||
|
||||
opened.clear()
|
||||
document["Labels"] = {"itworx.purpose": "different-volume"}
|
||||
assert status_module._forge_release_store_identity() is None
|
||||
assert opened == []
|
||||
|
||||
|
||||
def test_backup_volume_verifier_requires_its_read_only_none_contract(
|
||||
status_module, monkeypatch
|
||||
):
|
||||
backup = types.ModuleType("forge_backup_paths")
|
||||
backup.validate_forge_backup_volume = lambda: None
|
||||
monkeypatch.setitem(sys.modules, "forge_backup_paths", backup)
|
||||
assert status_module._forge_fixed_backup_volume_is_ready() is True
|
||||
backup.validate_forge_backup_volume = lambda: True
|
||||
assert status_module._forge_fixed_backup_volume_is_ready() is False
|
||||
@@ -267,6 +267,36 @@ def test_forge_bootstrap_preflight_is_read_only_and_rejects_generic_apply(integr
|
||||
json={"operation": "apply", "plan": profile},
|
||||
)
|
||||
assert generic_apply.status_code == 422
|
||||
generic_rollback = client.post(
|
||||
"/api/deployments",
|
||||
headers={**human_headers(client), "Idempotency-Key": "forge-generic-rollback-0001"},
|
||||
json={"operation": "rollback", "plan": profile},
|
||||
)
|
||||
assert generic_rollback.status_code == 422
|
||||
|
||||
apply_profile = {
|
||||
"api_version": "itworx.forge-first-install/v1",
|
||||
"revision": profile["revision"],
|
||||
"workspace_image_digest": profile["workspace_image_digest"],
|
||||
"frontdoor_image_digest": profile["frontdoor_image_digest"],
|
||||
"preflight_job_id": result["id"],
|
||||
"preflight_sha256": result["preflightSha256"],
|
||||
}
|
||||
dedicated_apply = client.post(
|
||||
"/api/deployments/forge-bootstrap/first-install",
|
||||
headers={**human_headers(client), "Idempotency-Key": "forge-api-install-0001"},
|
||||
json=apply_profile,
|
||||
)
|
||||
assert dedicated_apply.status_code == 409
|
||||
assert "passed Forge preflight" in dedicated_apply.json()["detail"]
|
||||
assert client.get(
|
||||
"/api/deployments/forge-bootstrap/first-install/forge-install-" + "0" * 64,
|
||||
headers=human_headers(client),
|
||||
).status_code == 404
|
||||
assert client.get(
|
||||
"/api/deployments/forge-install-" + "0" * 64,
|
||||
headers=human_headers(client),
|
||||
).status_code == 404
|
||||
|
||||
async def exercise_mcp():
|
||||
async with Client(mcp_server.appops_mcp) as mcp_client:
|
||||
@@ -280,6 +310,23 @@ def test_forge_bootstrap_preflight_is_read_only_and_rejects_generic_apply(integr
|
||||
{"plan": profile, "idempotencyKey": "forge-mcp-apply-0001"},
|
||||
)
|
||||
assert apply.structured_content["status"] == "invalid"
|
||||
forge_apply = await mcp_client.call_tool(
|
||||
"forge.bootstrap.apply",
|
||||
{"profile": apply_profile, "idempotencyKey": "forge-mcp-install-0001"},
|
||||
)
|
||||
assert forge_apply.structured_content["status"] == "invalid"
|
||||
forge_status = await mcp_client.call_tool(
|
||||
"forge.bootstrap.status", {"jobId": "forge-install-" + "0" * 64}
|
||||
)
|
||||
assert forge_status.structured_content["status"] == "not_found"
|
||||
generic_forge_status = await mcp_client.call_tool(
|
||||
"deployment.status", {"jobId": "forge-install-" + "0" * 64}
|
||||
)
|
||||
assert generic_forge_status.structured_content["status"] == "invalid"
|
||||
forge_resume = await mcp_client.call_tool(
|
||||
"forge.bootstrap.resume", {"jobId": "forge-install-" + "0" * 64}
|
||||
)
|
||||
assert forge_resume.structured_content["status"] == "not_found"
|
||||
|
||||
asyncio.run(exercise_mcp())
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CONTROLLER_ROOT = ROOT / "ops" / "gitea-deploy-broker"
|
||||
CONTROLLER_PATH = CONTROLLER_ROOT / "runner_capacity_controller.py"
|
||||
@@ -123,8 +123,18 @@ def test_host_snapshot_handles_absent_load_tuple(capacity_module, monkeypatch):
|
||||
assert result["host"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", ["", "pswpin 1\n", "pswpin -1\npswpout 2\n",
|
||||
"pswpin 1\npswpin 2\npswpout 3\n", "pswpin invalid\npswpout 2\n", "x" * 65537])
|
||||
@pytest.mark.parametrize(
|
||||
"raw",
|
||||
[
|
||||
"",
|
||||
"pswpin 1\n",
|
||||
"pswpin -1\npswpout 2\n",
|
||||
"pswpin 1\npswpin 2\npswpout 3\n",
|
||||
"pswpin invalid\npswpout 2\n",
|
||||
"x" * 65537,
|
||||
],
|
||||
ids=["empty", "missing-counter", "negative", "duplicate", "invalid", "oversized"],
|
||||
)
|
||||
def test_strict_swap_reader_rejects_missing_or_invalid_counters(capacity_module, monkeypatch, raw):
|
||||
monkeypatch.setattr(capacity_module, "Path", lambda _path: types.SimpleNamespace(
|
||||
open=lambda **_kwargs: io.StringIO(raw)
|
||||
@@ -161,6 +171,96 @@ def test_host_snapshot_rejects_invalid_global_metrics(capacity_module, monkeypat
|
||||
assert result["host"] is None
|
||||
|
||||
|
||||
def test_snapshot_and_cycle_treat_empty_optional_burst_inspect_as_not_configured(
|
||||
capacity_module, monkeypatch
|
||||
):
|
||||
service = capacity_module.RunnerCapacityController()
|
||||
commands = []
|
||||
state_reads = []
|
||||
|
||||
def inspect_only(arguments, **kwargs):
|
||||
commands.append((arguments, kwargs))
|
||||
assert arguments == ["docker", "inspect", capacity_module.BURST_RUNNER_NAME]
|
||||
assert kwargs == {"check": False}
|
||||
return "[]"
|
||||
|
||||
def unexpected(*_args, **_kwargs):
|
||||
pytest.fail("an absent optional burst runner must not reach registration, state, or mutation")
|
||||
|
||||
monkeypatch.setattr(service, "_run", inspect_only)
|
||||
monkeypatch.setattr(service, "_read_state", lambda: state_reads.append("read") or {})
|
||||
for name in ("_runner_registration", "_gitea_request", "_write_state", "_record", "_set_disabled"):
|
||||
monkeypatch.setattr(service, name, unexpected)
|
||||
|
||||
snapshot = service.snapshot()
|
||||
result = service.cycle()
|
||||
|
||||
assert snapshot["status"] == "not_configured"
|
||||
assert snapshot["runner"] == capacity_module.BURST_RUNNER_NAME
|
||||
assert result["status"] == "not_configured"
|
||||
assert result["runner"] == capacity_module.BURST_RUNNER_NAME
|
||||
assert state_reads == ["read"]
|
||||
assert commands == [
|
||||
(["docker", "inspect", capacity_module.BURST_RUNNER_NAME], {"check": False}),
|
||||
(["docker", "inspect", capacity_module.BURST_RUNNER_NAME], {"check": False}),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw", "reason"),
|
||||
[
|
||||
("not-json", "runner_capacity_container_invalid"),
|
||||
("{}", "runner_capacity_container_invalid"),
|
||||
(
|
||||
json.dumps([
|
||||
{"Config": {"Labels": {
|
||||
"com.docker.compose.project": "gitea-deploy-runners",
|
||||
"tech.itworx.role": "gitea-actions-burst-runner",
|
||||
}}, "State": {}},
|
||||
{"Config": {"Labels": {
|
||||
"com.docker.compose.project": "gitea-deploy-runners",
|
||||
"tech.itworx.role": "gitea-actions-burst-runner",
|
||||
}}, "State": {}},
|
||||
]),
|
||||
"runner_capacity_container_invalid",
|
||||
),
|
||||
(
|
||||
json.dumps([{
|
||||
"Config": {"Labels": {
|
||||
"com.docker.compose.project": "foreign-project",
|
||||
"tech.itworx.role": "gitea-actions-burst-runner",
|
||||
}},
|
||||
"State": {},
|
||||
}]),
|
||||
"runner_capacity_container_identity_invalid",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_cycle_still_refuses_malformed_foreign_or_multiple_burst_inspect_results(
|
||||
capacity_module, monkeypatch, raw, reason
|
||||
):
|
||||
service = capacity_module.RunnerCapacityController()
|
||||
commands = []
|
||||
|
||||
def inspect_only(arguments, **kwargs):
|
||||
commands.append((arguments, kwargs))
|
||||
return raw
|
||||
|
||||
def unexpected(*_args, **_kwargs):
|
||||
pytest.fail("invalid burst inventory must stop before registration or mutation")
|
||||
|
||||
monkeypatch.setattr(service, "_run", inspect_only)
|
||||
for name in ("_runner_registration", "_gitea_request", "_write_state", "_record", "_set_disabled"):
|
||||
monkeypatch.setattr(service, name, unexpected)
|
||||
|
||||
with pytest.raises(capacity_module.RunnerCapacityError, match=reason):
|
||||
service.cycle()
|
||||
|
||||
assert commands == [
|
||||
(["docker", "inspect", capacity_module.BURST_RUNNER_NAME], {"check": False})
|
||||
]
|
||||
|
||||
|
||||
def _wire_common(service, monkeypatch, *, running=False, busy=False, disabled=True):
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
@@ -183,7 +283,7 @@ def _wire_common(service, monkeypatch, *, running=False, busy=False, disabled=Tr
|
||||
"labels": ["linux-validation", "ubuntu-latest"],
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(service, "_read_state", lambda: {})
|
||||
monkeypatch.setattr(service, "_read_state", dict)
|
||||
monkeypatch.setattr(service, "_write_state", lambda _state: None)
|
||||
monkeypatch.setattr(service, "_record", lambda _event: None)
|
||||
monkeypatch.setattr(service, "_host_guard_state", lambda: ("clear", ""))
|
||||
|
||||
@@ -421,6 +421,119 @@ def test_successful_main_run_accepts_actual_gitea_run_without_workflow_id(
|
||||
assert calls[1].endswith("/actions/runs?branch=main&limit=100&page=1")
|
||||
|
||||
|
||||
def test_source_image_fetch_accepts_actual_gitea_commit_tree_shape(
|
||||
specialize, monkeypatch, tmp_path
|
||||
):
|
||||
revision = "a" * 40
|
||||
tree = "b" * 40
|
||||
calls = []
|
||||
workflow_calls = []
|
||||
|
||||
def api(path):
|
||||
calls.append(path)
|
||||
if path.endswith("/branches/main"):
|
||||
return {"commit": {"id": revision}}
|
||||
if path.endswith(f"/git/commits/{revision}"):
|
||||
return {"sha": revision, "commit": {"tree": {"sha": tree}}}
|
||||
if "/actions/runs/" in path and "/artifacts?" in path:
|
||||
return {
|
||||
"artifacts": [
|
||||
{
|
||||
"name": specialize.IMAGE_ARTIFACT_PREFIX + revision,
|
||||
"expired": False,
|
||||
"id": 44,
|
||||
"size_in_bytes": 1,
|
||||
}
|
||||
]
|
||||
}
|
||||
raise AssertionError(f"unexpected fixed Gitea API path: {path}")
|
||||
|
||||
def successful_run(workflow_path, requested_revision):
|
||||
assert requested_revision == revision
|
||||
workflow_calls.append(workflow_path)
|
||||
return {"id": 41 if workflow_path == specialize.IMAGE_WORKFLOW else 42}
|
||||
|
||||
class ReachedArtifactDownload(Exception):
|
||||
pass
|
||||
|
||||
def download(_artifact_id, _destination):
|
||||
raise ReachedArtifactDownload
|
||||
|
||||
monkeypatch.setattr(specialize, "_api_json", api)
|
||||
monkeypatch.setattr(specialize, "_successful_main_run", successful_run)
|
||||
monkeypatch.setattr(specialize, "_download_artifact_zip", download)
|
||||
|
||||
with pytest.raises(ReachedArtifactDownload):
|
||||
specialize._fetch_source_image_artifact(revision, tree, tmp_path)
|
||||
|
||||
assert workflow_calls == [specialize.IMAGE_WORKFLOW, specialize.FULL_WORKFLOW]
|
||||
assert calls == [
|
||||
f"/repos/{specialize.SOURCE_REPOSITORY_API}/branches/main",
|
||||
f"/repos/{specialize.SOURCE_REPOSITORY_API}/git/commits/{revision}",
|
||||
f"/repos/{specialize.SOURCE_REPOSITORY_API}/actions/runs/41/artifacts?name={specialize.IMAGE_ARTIFACT_PREFIX}{revision}",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("commit_value", "error"),
|
||||
[
|
||||
({"sha": "a" * 40}, "source_tree_mismatch"),
|
||||
({"sha": "a" * 40, "commit": {}}, "source_tree_mismatch"),
|
||||
(
|
||||
{"sha": "a" * 40, "commit": {"tree": {"sha": "c" * 40}}},
|
||||
"source_tree_mismatch",
|
||||
),
|
||||
(
|
||||
{"sha": "a" * 40, "tree": {"sha": "b" * 40}},
|
||||
"source_tree_mismatch",
|
||||
),
|
||||
(
|
||||
{"sha": "c" * 40, "commit": {"tree": {"sha": "b" * 40}}},
|
||||
"source_revision_mismatch",
|
||||
),
|
||||
(
|
||||
{"commit": {"tree": {"sha": "b" * 40}}},
|
||||
"source_revision_mismatch",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_source_image_fetch_rejects_noncanonical_gitea_commit_payload_before_artifact_lookup(
|
||||
specialize, monkeypatch, tmp_path, commit_value, error
|
||||
):
|
||||
revision = "a" * 40
|
||||
calls = []
|
||||
workflow_calls = []
|
||||
|
||||
def api(path):
|
||||
calls.append(path)
|
||||
if path.endswith("/branches/main"):
|
||||
return {"commit": {"id": revision}}
|
||||
if path.endswith(f"/git/commits/{revision}"):
|
||||
return commit_value
|
||||
raise AssertionError(f"unexpected fixed Gitea API path: {path}")
|
||||
|
||||
def unexpected_workflow(*_args):
|
||||
workflow_calls.append(True)
|
||||
raise AssertionError("workflow lookup must not follow an untrusted commit response")
|
||||
|
||||
monkeypatch.setattr(specialize, "_api_json", api)
|
||||
monkeypatch.setattr(specialize, "_successful_main_run", unexpected_workflow)
|
||||
monkeypatch.setattr(
|
||||
specialize,
|
||||
"_download_artifact_zip",
|
||||
lambda *_args: pytest.fail("artifact download must not follow an untrusted commit response"),
|
||||
)
|
||||
|
||||
with pytest.raises(specialize.RunnerFleetSpecializeError, match=error):
|
||||
specialize._fetch_source_image_artifact(revision, "b" * 40, tmp_path)
|
||||
|
||||
assert calls == [
|
||||
f"/repos/{specialize.SOURCE_REPOSITORY_API}/branches/main",
|
||||
f"/repos/{specialize.SOURCE_REPOSITORY_API}/git/commits/{revision}",
|
||||
]
|
||||
assert workflow_calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes",
|
||||
[
|
||||
|
||||
@@ -93,12 +93,59 @@ def test_portainer_config_save_never_returns_secret(api_client):
|
||||
"verifyTls": False,
|
||||
}
|
||||
assert secret not in str(payload)
|
||||
|
||||
all_settings = api_client.get("/api/settings/connectors").json()
|
||||
assert secret not in str(all_settings)
|
||||
assert next(item for item in all_settings if item["connectorId"] == "portainer")["hasSecret"] is True
|
||||
|
||||
|
||||
def test_generic_authentik_setup_cannot_set_or_overwrite_forge_oidc_binding(api_client):
|
||||
import json
|
||||
|
||||
from sqlmodel import Session
|
||||
|
||||
from backend.app.db import session as db_session
|
||||
from backend.app.schemas.forge_oidc import FORGE_OIDC_BINDING_SECRET_REF
|
||||
from backend.app.services.secret_store import get_secret, set_secret
|
||||
from backend.app.services.setup_service import _apply_forge_oidc_binding_to_runtime
|
||||
from backend.app.core.config import settings
|
||||
|
||||
binding = json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"providerId": "31",
|
||||
"scopeMappingIds": {
|
||||
"openid": "75a34fd6-cc81-40b8-a8c3-2745af9567ab",
|
||||
"email": "312810bb-11da-47d8-97d4-f2155348eaa2",
|
||||
"profile": "7aa8c5f8-5e8e-4a1b-9c11-3a4f0a731cff",
|
||||
},
|
||||
}
|
||||
)
|
||||
with Session(db_session.engine) as session:
|
||||
set_secret(session, FORGE_OIDC_BINDING_SECRET_REF, binding)
|
||||
|
||||
rejected = api_client.patch(
|
||||
"/api/settings/connectors/authentik",
|
||||
json={"forgeOidcBinding": binding, "providerId": "999"},
|
||||
)
|
||||
assert rejected.status_code == 422
|
||||
|
||||
saved = api_client.patch(
|
||||
"/api/settings/connectors/authentik",
|
||||
json={"url": "https://auth.example-host.com", "secret": "GENERIC_AUTHENTIK_TOKEN"},
|
||||
)
|
||||
assert saved.status_code == 200
|
||||
assert binding not in saved.text
|
||||
assert "GENERIC_AUTHENTIK_TOKEN" not in saved.text
|
||||
with Session(db_session.engine) as session:
|
||||
assert get_secret(session, FORGE_OIDC_BINDING_SECRET_REF, touch=False) == binding
|
||||
assert _apply_forge_oidc_binding_to_runtime(session) is True
|
||||
assert settings.apis["AUTHENTIK_FORGE_OIDC_PROVIDER_ID"] == "31"
|
||||
|
||||
all_settings = api_client.get("/api/settings/connectors").json()
|
||||
assert binding not in str(all_settings)
|
||||
assert "GENERIC_AUTHENTIK_TOKEN" not in str(all_settings)
|
||||
|
||||
|
||||
def test_portainer_config_save_trims_safe_fields(api_client):
|
||||
payload = api_client.patch(
|
||||
"/api/settings/connectors/portainer",
|
||||
|
||||
Reference in new issue
Block a user