Publish curated Unraid-AppOps-Gateway source from 6d175c3333218e2b580a310496fed6c407f28114
This commit is contained in:
1 parent
58f5e3c8c2
commit
41fd48f0bc
7 files changed
+339
-16
No files matched your search
@@ -1135,3 +1135,36 @@ def test_followup_transaction_journal_failure_precedes_all_source_runtime_and_st
|
||||
assert [event[0] for event in events] == ["green", "snapshot", "backup", "journal"]
|
||||
assert marker.read_text() == "prior source remains\n"
|
||||
assert state["live_revision"] == prior_revision
|
||||
|
||||
|
||||
|
||||
def test_prepare_restored_recovery_rechecks_signed_live_state_and_recaptures_under_new_id(
|
||||
deploy, monkeypatch, tmp_path,
|
||||
):
|
||||
case = _appops_prepare_recovery_case(deploy, monkeypatch, tmp_path)
|
||||
calls = []
|
||||
|
||||
def verify(_project, container, image, **kwargs):
|
||||
calls.append(kwargs)
|
||||
assert container["Id"] != deploy.APPOPS_LEGACY_OLD_CONTAINER_ID
|
||||
|
||||
monkeypatch.setattr(deploy, "verify_appops_legacy_runtime", verify)
|
||||
deploy._appops_prepare_recovery(
|
||||
case.project, APP_OPS_REPOSITORY, case.repo, case.revision, case.metadata, case.state,
|
||||
)
|
||||
assert calls == [{"require_original_container": False, "recovery_state": case.state}]
|
||||
assert case.calls["models"][0][0] == RECOVERY_ID
|
||||
assert case.calls["models"][0][2]["services"]["appops-gateway"]["environment"] == case.values
|
||||
assert case.project["_compose_rollback"]["containers"][0]["container_id"] == case.container["Id"]
|
||||
|
||||
|
||||
def test_prepare_recovery_rejects_environment_drift_before_model_backup_or_transaction(
|
||||
deploy, monkeypatch, tmp_path,
|
||||
):
|
||||
case = _appops_prepare_recovery_case(deploy, monkeypatch, tmp_path)
|
||||
case.container["Config"]["Env"].append("UNEXPECTED_SETTING=private-sentinel")
|
||||
with pytest.raises(deploy.DeployError, match="complete runtime environment changed"):
|
||||
deploy._appops_prepare_recovery(
|
||||
case.project, APP_OPS_REPOSITORY, case.repo, case.revision, case.metadata, case.state,
|
||||
)
|
||||
assert not any(case.calls[name] for name in ("models", "capture", "backup", "begin"))
|
||||
@@ -556,3 +556,166 @@ def test_original_failed_delivery_rejects_modified_legacy_record(controller, mon
|
||||
assert controller._self_upgrade_original_failed_delivery(
|
||||
semantic, revision, policy,
|
||||
) is None
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("status", "extra", "valid"), [
|
||||
("deploying", {}, True),
|
||||
("failed", {"reason": "deployment_failed"}, True),
|
||||
("deployed", {"receipt_sha256": "f" * 64}, True),
|
||||
("deployed", {"receipt_sha256": "invalid"}, False),
|
||||
("deploying", {"reason": "deployment_failed"}, False),
|
||||
("failed", {"reason": "private-secret-value"}, False),
|
||||
("failed", {"receipt_sha256": "f" * 64}, False),
|
||||
("failed", {"unknown": "value"}, False),
|
||||
("failed", {"reason": ["deployment_failed"]}, False),
|
||||
("awaiting_validation", {"reason": "gitea_validation_unavailable"}, True),
|
||||
("failed", {"reason": "recovery_preimage_changed"}, True),
|
||||
])
|
||||
def test_signed_recovery_reader_accepts_only_typed_worker_outcome_fields(
|
||||
controller, monkeypatch, tmp_path, status, extra, valid,
|
||||
):
|
||||
record = _recovery_delivery(controller, secret="sentinel")
|
||||
record.pop("runtimeEnvironment")
|
||||
record.pop("privateCredential")
|
||||
record.update(status=status, **extra)
|
||||
secret = b"fixture-recovery-signing-key"
|
||||
journal = {
|
||||
"recoveryId": record["recovery_id"], "revision": record["revision"],
|
||||
"idempotencyKeySha256": record["idempotency_key_sha256"],
|
||||
"preflightSha256": record["preflight_sha256"],
|
||||
"semanticSha256": record["semantic_sha256"],
|
||||
"originalRawSha256": record["recovery_of_sha256"],
|
||||
}
|
||||
payload = {
|
||||
"operationId": record["semantic_sha256"], "revision": record["revision"],
|
||||
"preflightSha256": record["preflight_sha256"],
|
||||
"idempotencyKeySha256": record["idempotency_key_sha256"],
|
||||
"recoveryId": record["recovery_id"],
|
||||
"recoveryOfSha256": record["recovery_of_sha256"],
|
||||
}
|
||||
record["body_sha256"] = controller.hashlib.sha256(controller._self_upgrade_canonical_json({
|
||||
"semanticSha256": record["semantic_sha256"], "recoveryId": record["recovery_id"],
|
||||
"idempotencyKeySha256": record["idempotency_key_sha256"],
|
||||
})).hexdigest()
|
||||
record["recovery_hmac"] = controller._self_upgrade_public_recovery_signature(record, secret)
|
||||
monkeypatch.setattr(controller, "_self_upgrade_secret", lambda: secret)
|
||||
monkeypatch.setattr(controller, "_self_upgrade_recovery_journal", lambda _id: journal)
|
||||
monkeypatch.setattr(controller, "_load_projects", lambda: {controller.SELF_UPGRADE_REPOSITORY: {}})
|
||||
monkeypatch.setattr(controller, "_validation_policy", lambda *_a: {})
|
||||
monkeypatch.setattr(controller, "_validation_snapshot_matches", lambda *_a: True)
|
||||
monkeypatch.setattr(controller, "_self_upgrade_request_root", lambda **_k: tmp_path)
|
||||
monkeypatch.setattr(controller, "_self_upgrade_read_private_json", lambda _path: (b"", {}))
|
||||
monkeypatch.setattr(controller, "_self_upgrade_recovery_request_payload", lambda _record: payload)
|
||||
|
||||
def verify():
|
||||
return controller._self_upgrade_recovery_record_valid(
|
||||
record, semantic_digest=record["semantic_sha256"],
|
||||
original_raw_sha256=record["recovery_of_sha256"],
|
||||
)
|
||||
|
||||
assert verify() is valid
|
||||
record["updated_at"] += 1
|
||||
assert verify() is False # Even permitted outcome fields require the original complete HMAC.
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("uid", "gid", "mode"), [(99, 100, 0o777), (0, 0, 0o755)])
|
||||
@pytest.mark.parametrize("drift", ["none", "owner", "mode", "symlink", "foreign_cache", "inode"])
|
||||
def test_legacy_cache_reader_and_state_cas_bind_both_exact_appdata_profiles(
|
||||
controller, monkeypatch, tmp_path, uid, gid, mode, drift,
|
||||
):
|
||||
if os.name == "nt" or os.geteuid() != 0:
|
||||
pytest.skip("Pinned dirfd ownership contract requires root POSIX")
|
||||
root = tmp_path / "virtual-host"
|
||||
root.mkdir(mode=0o755)
|
||||
dirs = [
|
||||
("mnt", 0, 0, 0o755), ("mnt/cache", 99, 100, 0o777),
|
||||
("mnt/cache/appdata", uid, gid, mode),
|
||||
("mnt/cache/appdata/gitea-deploy-runners", 0, 0, 0o755),
|
||||
("mnt/cache/appdata/gitea-deploy-runners/state", 0, 0, 0o755),
|
||||
]
|
||||
for name, owner, group, permissions in dirs:
|
||||
path = root / name
|
||||
path.mkdir()
|
||||
os.chown(path, owner, group)
|
||||
path.chmod(permissions)
|
||||
appdata = root / "mnt/cache/appdata"
|
||||
state_root = appdata / "gitea-deploy-runners/state"
|
||||
state_path = state_root / "unraid-appops-gateway.json"
|
||||
before = controller._self_upgrade_canonical_json({
|
||||
"schema_version": 3, "repository": controller.SELF_UPGRADE_REPOSITORY, "marker": "old",
|
||||
}) + b"\n"
|
||||
state_path.write_bytes(before)
|
||||
state_path.chmod(0o600)
|
||||
after = before.replace(b'"old"', b'"new"')
|
||||
if drift == "owner":
|
||||
os.chown(appdata, 1000, 1000)
|
||||
elif drift == "mode":
|
||||
appdata.chmod(0o775)
|
||||
elif drift == "foreign_cache":
|
||||
os.chown(root / "mnt/cache", 0, 0)
|
||||
(root / "mnt/cache").chmod(0o755)
|
||||
elif drift == "inode":
|
||||
replacement = appdata.with_name("different-inode")
|
||||
replacement.mkdir()
|
||||
os.chown(replacement, uid, gid)
|
||||
replacement.chmod(mode)
|
||||
elif drift == "symlink":
|
||||
renamed = appdata.with_name("appdata-elsewhere")
|
||||
appdata.rename(renamed)
|
||||
appdata.symlink_to(renamed, target_is_directory=True)
|
||||
monkeypatch.setattr(controller, "STATE_ROOT", Path("/mnt/cache/appdata/gitea-deploy-runners/state"))
|
||||
real_open = os.open
|
||||
|
||||
def virtual_root_open(path, flags, *args, **kwargs):
|
||||
if drift == "inode" and str(path) == "appdata":
|
||||
return real_open(replacement, flags)
|
||||
return real_open(root if str(path) == "/" else path, flags, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(controller.os, "open", virtual_root_open)
|
||||
requested = controller.STATE_ROOT / state_path.name
|
||||
if drift == "none":
|
||||
assert controller._self_upgrade_read_legacy_cache_file(
|
||||
requested, maximum_bytes=65536, allowed_modes=frozenset({0o600}),
|
||||
) == before
|
||||
assert not controller._self_upgrade_write_legacy_state_cas("0" * 64, after)
|
||||
assert state_path.read_bytes() == before
|
||||
assert controller._self_upgrade_write_legacy_state_cas(
|
||||
controller.hashlib.sha256(before).hexdigest(), after,
|
||||
)
|
||||
assert state_path.read_bytes() == after
|
||||
else:
|
||||
with pytest.raises(OSError):
|
||||
controller._self_upgrade_read_legacy_cache_file(
|
||||
requested, maximum_bytes=65536, allowed_modes=frozenset({0o600}),
|
||||
)
|
||||
assert not controller._self_upgrade_write_legacy_state_cas(
|
||||
controller.hashlib.sha256(before).hexdigest(), after,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("profile", "valid"), [
|
||||
((99, 100, 0o777), True), ((0, 0, 0o755), True),
|
||||
((0, 0, 0o775), False), ((99, 100, 0o755), False),
|
||||
])
|
||||
def test_source_root_allows_only_legacy_or_root_owned_appdata_anchor(
|
||||
controller, monkeypatch, profile, valid,
|
||||
):
|
||||
from types import SimpleNamespace
|
||||
root = Path("/srv/example/appdata/unraid-appops-gateway/source")
|
||||
profiles = {
|
||||
Path("/mnt"): (0, 0, 0o755), Path("/mnt/user"): (99, 100, 0o777),
|
||||
Path("/srv/example/appdata"): profile,
|
||||
root.parent: (0, 0, 0o755), root: (100, 101, 0o755),
|
||||
}
|
||||
|
||||
def lstat(path):
|
||||
uid, gid, mode = profiles[path]
|
||||
return SimpleNamespace(st_uid=uid, st_gid=gid, st_mode=stat.S_IFDIR | mode)
|
||||
|
||||
monkeypatch.setattr(Path, "lstat", lstat)
|
||||
monkeypatch.setattr(Path, "is_symlink", lambda _path: False)
|
||||
assert controller._self_upgrade_source_root_safe(root) is valid
|
||||
monkeypatch.setattr(Path, "is_symlink", lambda path: path == root.parent)
|
||||
assert controller._self_upgrade_source_root_safe(root) is False
|
||||
@@ -379,9 +379,7 @@ def test_appops_legacy_state_anchor_accepts_only_known_preimage_or_current_green
|
||||
)
|
||||
|
||||
|
||||
def test_appops_legacy_runtime_requires_pinned_source_image_and_resolved_model(
|
||||
deploy_module, tmp_path
|
||||
):
|
||||
def _fixed_appops_legacy_runtime_case(deploy_module, tmp_path):
|
||||
project = _project(tmp_path)
|
||||
project.update({
|
||||
"branch": "main", "gitea_repository_id": 9,
|
||||
@@ -435,6 +433,15 @@ def test_appops_legacy_runtime_requires_pinned_source_image_and_resolved_model(
|
||||
"Config": {"Labels": container["Config"]["Labels"]},
|
||||
}
|
||||
|
||||
return project, container, image
|
||||
|
||||
|
||||
def test_appops_legacy_runtime_requires_pinned_source_image_and_resolved_model(
|
||||
deploy_module, tmp_path
|
||||
):
|
||||
project, container, image = _fixed_appops_legacy_runtime_case(deploy_module, tmp_path)
|
||||
image_id = deploy_module.APPOPS_LEGACY_OLD_IMAGE_ID
|
||||
source = "https://gitea.example.com/Jens/Unraid-AppOps-Gateway"
|
||||
observed = deploy_module.verify_appops_legacy_runtime(project, container, image)
|
||||
assert observed["imageId"] == image_id
|
||||
assert observed["imageRef"] == "unraid-appops-gateway:platform-5553c04ce497"
|
||||
@@ -10604,3 +10611,67 @@ def test_managed_appdata_initializer_http_prerequisite_and_refusal_are_sanitized
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
serving.join(timeout=2)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize("proof", ["signed", "missing", "foreign", "candidate"])
|
||||
def test_recovery_verifies_restored_container_only_with_signed_live_555_model(
|
||||
deploy_module, monkeypatch, tmp_path, proof,
|
||||
):
|
||||
project, container, image = _fixed_appops_legacy_runtime_case(deploy_module, tmp_path)
|
||||
container["Id"] = "a" * 64
|
||||
model_path = "/private/models/signed-555.compose.json"
|
||||
container["Config"]["Labels"]["com.docker.compose.project.config_files"] = model_path
|
||||
state = {"live_revision": deploy_module.APPOPS_LEGACY_OLD_REVISION}
|
||||
active = {"legacy": True, "path": model_path}
|
||||
if proof == "missing":
|
||||
active = None
|
||||
elif proof == "foreign":
|
||||
active["path"] = "/private/models/foreign.compose.json"
|
||||
elif proof == "candidate":
|
||||
active["legacy"] = False
|
||||
calls = []
|
||||
|
||||
def verify_active(actual_state, actual_container, *, project):
|
||||
calls.append((actual_state, actual_container, project))
|
||||
return active
|
||||
|
||||
reader = types.SimpleNamespace(_self_upgrade_active_compose_model=verify_active)
|
||||
monkeypatch.setattr(deploy_module, "_appops_status_reader", lambda: reader)
|
||||
with pytest.raises(deploy_module.DeployError, match="fixed AppOps legacy runtime"):
|
||||
deploy_module.verify_appops_legacy_runtime(project, container, image)
|
||||
if proof == "signed":
|
||||
observed = deploy_module.verify_appops_legacy_runtime(
|
||||
project, container, image, require_original_container=False, recovery_state=state,
|
||||
)
|
||||
assert observed["containerId"] == container["Id"]
|
||||
else:
|
||||
with pytest.raises(deploy_module.DeployError):
|
||||
deploy_module.verify_appops_legacy_runtime(
|
||||
project, container, image, require_original_container=False, recovery_state=state,
|
||||
)
|
||||
assert calls == [(state, container, project)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("drift", ["image", "ports", "mounts", "health"])
|
||||
def test_signed_restored_555_proof_preserves_fixed_runtime_checks(
|
||||
deploy_module, monkeypatch, tmp_path, drift,
|
||||
):
|
||||
project, container, image = _fixed_appops_legacy_runtime_case(deploy_module, tmp_path)
|
||||
container["Id"] = "a" * 64
|
||||
active = {"legacy": True, "path": "/private/models/signed-555.compose.json"}
|
||||
monkeypatch.setattr(deploy_module, "_appops_status_reader", lambda: types.SimpleNamespace(
|
||||
_self_upgrade_active_compose_model=lambda *_a, **_k: active,
|
||||
))
|
||||
if drift == "image":
|
||||
image["Id"] = "sha256:" + "0" * 64
|
||||
elif drift == "ports":
|
||||
container["NetworkSettings"]["Ports"]["1216/tcp"][0]["HostIp"] = "127.0.0.1"
|
||||
elif drift == "mounts":
|
||||
container["Mounts"][0]["RW"] = False
|
||||
elif drift == "health":
|
||||
container["State"]["Health"]["Status"] = "unhealthy"
|
||||
with pytest.raises(deploy_module.DeployError):
|
||||
deploy_module.verify_appops_legacy_runtime(
|
||||
project, container, image, require_original_container=False, recovery_state={},
|
||||
)
|
||||
@@ -1369,3 +1369,44 @@ def test_controller_preflight_changed_identity_expires_approval_without_unknown_
|
||||
promotion = session.get(WorkMeshPromotion, promotion_id)
|
||||
assert approval is not None and approval.status == "expired"
|
||||
assert promotion is not None and promotion.status == "preflight_changed"
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("name", "argument", "discriminator"), [
|
||||
("enrollment.preflight", "plan", None),
|
||||
("target.enroll", "plan", None),
|
||||
("deployment.preflight", "plan", None),
|
||||
("deployment.apply", "plan", None),
|
||||
("deployment.rollback", "plan", None),
|
||||
("deployment.preflight", "plan", "itworx.forge-bootstrap-preflight/v1"),
|
||||
("forge.bootstrap.apply", "profile", None),
|
||||
])
|
||||
def test_mcp_typed_schema_rejection_never_echoes_opaque_input(
|
||||
integration_client, monkeypatch, name, argument, discriminator,
|
||||
):
|
||||
from backend.app import mcp_server
|
||||
canary = "syntheticOpaqueInput_9Z7XQ"
|
||||
payload = {"api_key": canary, canary: {"nested": canary}, "application_id": canary}
|
||||
if discriminator:
|
||||
payload["api_version"] = discriminator
|
||||
arguments = {argument: payload}
|
||||
if name != "enrollment.preflight":
|
||||
arguments["idempotencyKey"] = "schema-redaction-fixture"
|
||||
|
||||
def forbidden_service(*_args, **_kwargs):
|
||||
pytest.fail("invalid schema must not reach deployment/enrollment services")
|
||||
|
||||
monkeypatch.setattr(mcp_server, "EnrollmentPlane", forbidden_service)
|
||||
monkeypatch.setattr(mcp_server, "DeploymentControlPlane", forbidden_service)
|
||||
|
||||
async def exercise():
|
||||
async with Client(mcp_server.appops_mcp) as client:
|
||||
result = await client.call_tool(name, arguments)
|
||||
assert result.is_error is False
|
||||
assert result.structured_content == {
|
||||
"status": "invalid", "executed": False, "reason": "invalid_typed_plan",
|
||||
}
|
||||
assert canary not in json.dumps(result.model_dump(mode="json"))
|
||||
|
||||
asyncio.run(exercise())
|
||||
assert mcp_server._MCP_EXECUTION_LOCK.locked() is False
|
||||
Reference in new issue
Block a user