Publish curated Unraid-AppOps-Gateway source from ba66029223865607deddc234e283c11127324eb8
This commit is contained in:
1 parent
820244dbb3
commit
8dd4b40252
52 files changed
+11814
-288
No files matched your search
@@ -764,6 +764,41 @@ def test_appops_legacy_intent_accepts_only_the_reviewed_dockerfile_blob_pair(
|
||||
deploy_module._appops_validate_intent_payload(unknown_update)
|
||||
|
||||
|
||||
def test_new_database_pair_keeps_protected_full_and_runtime_intent_proofs_strict(
|
||||
deploy_module,
|
||||
):
|
||||
_repository, _metadata, _source, _main, _backup, intent = (
|
||||
_appops_rollback_intent_fixture(deploy_module, "c" * 40, "a" * 64)
|
||||
)
|
||||
|
||||
def rebind_migration_proof(value):
|
||||
proof = value["migrationProof"]
|
||||
evidence = value["rollbackSnapshot"]["appops_legacy"]
|
||||
evidence["sourceProofSha256"] = deploy_module._canonical_json_sha256(proof["source"])
|
||||
evidence["protectedMainSha256"] = deploy_module._canonical_json_sha256(proof["protectedMain"])
|
||||
evidence["migrationProofSha256"] = deploy_module._canonical_json_sha256(proof)
|
||||
|
||||
intent["migrationProof"]["source"]["databaseTree"] = (
|
||||
"21d29c1799118fdc43946c2b6fd5585ccc2c4db1"
|
||||
)
|
||||
rebind_migration_proof(intent)
|
||||
deploy_module._appops_validate_intent_payload(intent)
|
||||
|
||||
weak_protected = copy.deepcopy(intent)
|
||||
weak_protected["migrationProof"]["protectedMain"]["validationContext"] = (
|
||||
"Managed validation / partial (push)"
|
||||
)
|
||||
rebind_migration_proof(weak_protected)
|
||||
with pytest.raises(deploy_module.DeployError, match="protected-main proof is invalid"):
|
||||
deploy_module._appops_validate_intent_payload(weak_protected)
|
||||
|
||||
incomplete_runtime = copy.deepcopy(intent)
|
||||
incomplete_runtime["migrationProof"]["runtime"].pop("healthSha256")
|
||||
rebind_migration_proof(incomplete_runtime)
|
||||
with pytest.raises(deploy_module.DeployError, match="legacy runtime proof is invalid"):
|
||||
deploy_module._appops_validate_intent_payload(incomplete_runtime)
|
||||
|
||||
|
||||
def test_appops_cli_rollback_retries_after_state_write_crash_without_synthetic_receipt(
|
||||
deploy_module, monkeypatch, tmp_path, capsys
|
||||
):
|
||||
@@ -1537,6 +1572,47 @@ def test_appops_state_write_rejects_compare_and_swap_drift(deploy_module, monkey
|
||||
assert path.read_bytes() == original
|
||||
|
||||
|
||||
def test_registered_legacy_state_cas_drift_preserves_external_state_and_records_recovery(
|
||||
deploy_module, monkeypatch, tmp_path
|
||||
):
|
||||
repository = "jens/quiz-monitor"
|
||||
request_id = "6f9619ff-8b86-4011-b42d-00c04fc964ff"
|
||||
revision = "a" * 40
|
||||
state_root = tmp_path / "state"
|
||||
state_root.mkdir()
|
||||
monkeypatch.setattr(deploy_module, "STATE_ROOT", state_root)
|
||||
monkeypatch.setattr(deploy_module, "_registered_legacy_receipt_key", lambda: b"k" * 32)
|
||||
project = {
|
||||
"slug": "quiz-monitor", "branch": "main", "compose_project": "quiz-monitor",
|
||||
"compose_files": ["docker-compose.yml"],
|
||||
"containers": ["quiz-monitor-quiz-server-1"],
|
||||
"source_dir": "/srv/example/appdata/quiz-monitor/source",
|
||||
"_registered_legacy_request_id": request_id,
|
||||
"_registered_legacy_authorization": {
|
||||
"requestId": request_id, "revision": revision,
|
||||
"expectedStateSha256": "0" * 64,
|
||||
},
|
||||
}
|
||||
path = state_root / "quiz-monitor.json"
|
||||
external_state = b'{"schema_version":3,"live_revision":"independent-update"}\n'
|
||||
path.write_bytes(external_state)
|
||||
|
||||
with pytest.raises(deploy_module.DeployError, match="compare-and-swap"):
|
||||
deploy_module.write_state(
|
||||
repository, project, revision, "failed", "deployment failed",
|
||||
expected_state_sha256="0" * 64,
|
||||
)
|
||||
|
||||
assert path.read_bytes() == external_state
|
||||
helper = deploy_module._load_registered_legacy_adoption()
|
||||
outcome = helper.status(
|
||||
{"repository": repository, "requestId": request_id},
|
||||
registry={repository: project}, receipt_key=b"k" * 32, state_root=state_root,
|
||||
)
|
||||
assert outcome["status"] == "recovery-required"
|
||||
assert outcome["reasonCode"] == "controller_state_cas_failed"
|
||||
|
||||
|
||||
def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
|
||||
"""Build the reviewed DB and Dockerfile transitions without old Git history."""
|
||||
repository = tmp_path / "reviewed-source.git"
|
||||
@@ -1601,6 +1677,7 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
|
||||
forge_seed_rows = (
|
||||
b' Policy(id="policy-forge-oidc-provision", action="forge.oidc.provision", target="itworx-forge", voice="blocked", manual="allowed", risk="critical", reason="The fixed Forge OIDC client may be provisioned only by an authenticated manual operator route; voice and generic Authentik mutations remain blocked."),\n',
|
||||
b' Policy(id="policy-forge-secret-prepare", action="forge.secrets.prepare", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Preparing the fixed Forge bootstrap secret bundle is default-denied and requires an explicit exact-target manual operator policy; voice remains blocked."),\n',
|
||||
b' Policy(id="policy-forge-route-stage", action="forge.route.stage", target="itworx-forge", voice="blocked", manual="blocked", risk="critical", reason="Staging the fixed disabled Forge proxy host is default-denied; voice remains blocked."),\n',
|
||||
)
|
||||
for row in forge_seed_rows:
|
||||
assert current_init.count(row) == 1
|
||||
@@ -1629,7 +1706,15 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
|
||||
old_tree = git("rev-parse", f"{old_revision}^{{tree}}")
|
||||
old_database_tree = git("rev-parse", f"{old_revision}:backend/app/db")
|
||||
|
||||
(db / "init_db.py").write_bytes(current_init)
|
||||
candidate_init = current_init
|
||||
if candidate_mutation == "previous_reviewed_seed_tree":
|
||||
route_seed = forge_seed_rows[2]
|
||||
assert candidate_init.count(route_seed) == 1
|
||||
candidate_init = candidate_init.replace(route_seed, b"", 1)
|
||||
elif candidate_mutation == "legacy_database_unchanged":
|
||||
candidate_init = old_init
|
||||
|
||||
(db / "init_db.py").write_bytes(candidate_init)
|
||||
if candidate_mutation == "extra_database_file":
|
||||
(db / "unreviewed.py").write_text("UNREVIEWED = True\n", encoding="utf-8")
|
||||
elif candidate_mutation == "models_content":
|
||||
@@ -1637,21 +1722,47 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
|
||||
elif candidate_mutation == "session_content":
|
||||
(db / "session.py").write_bytes(normalize(current_db / "session.py") + b"\n# unreviewed\n")
|
||||
elif candidate_mutation == "init_db_content":
|
||||
(db / "init_db.py").write_bytes(current_init + b"\n# unreviewed database behavior\n")
|
||||
(db / "init_db.py").write_bytes(candidate_init + b"\n# unreviewed database behavior\n")
|
||||
elif candidate_mutation == "forge_policy_changed":
|
||||
changed_init = current_init.replace(
|
||||
changed_init = candidate_init.replace(
|
||||
b'action="forge.oidc.provision"',
|
||||
b'action="forge.oidc.provision-unreviewed"',
|
||||
1,
|
||||
)
|
||||
assert changed_init != current_init
|
||||
(db / "init_db.py").write_bytes(changed_init)
|
||||
elif candidate_mutation in {
|
||||
"route_stage_manual_allowed", "route_stage_voice_allowed", "route_stage_risk_lowered",
|
||||
}:
|
||||
route_seed = forge_seed_rows[2]
|
||||
replacements = {
|
||||
"route_stage_manual_allowed": (b'manual="blocked"', b'manual="allowed"'),
|
||||
"route_stage_voice_allowed": (b'voice="blocked"', b'voice="allowed"'),
|
||||
"route_stage_risk_lowered": (b'risk="critical"', b'risk="high"'),
|
||||
}
|
||||
old_field, new_field = replacements[candidate_mutation]
|
||||
assert route_seed.count(old_field) == 1
|
||||
candidate_init = candidate_init.replace(route_seed, route_seed.replace(old_field, new_field, 1), 1)
|
||||
(db / "init_db.py").write_bytes(candidate_init)
|
||||
elif candidate_mutation == "unknown_policy_row":
|
||||
route_seed = forge_seed_rows[2]
|
||||
unknown_policy = (
|
||||
b' Policy(id="policy-forge-route-stage-unreviewed", '
|
||||
b'action="forge.route.stage.unreviewed", target="itworx-forge", '
|
||||
b'voice="blocked", manual="blocked", risk="critical", '
|
||||
b'reason="Unreviewed policy row."),\n'
|
||||
)
|
||||
assert candidate_init.count(route_seed) == 1
|
||||
candidate_init = candidate_init.replace(route_seed, route_seed + unknown_policy, 1)
|
||||
(db / "init_db.py").write_bytes(candidate_init)
|
||||
elif candidate_mutation == "database_file_mode":
|
||||
(db / "session.py").chmod(0o755)
|
||||
elif candidate_mutation == "entrypoint_content":
|
||||
entrypoint.write_bytes(entrypoint.read_bytes() + b"# unreviewed entrypoint\n")
|
||||
elif candidate_mutation not in {
|
||||
None, "dockerfile_one_byte", "dockerfile_legacy_unchanged",
|
||||
None, "previous_reviewed_seed_tree", "legacy_database_unchanged",
|
||||
"route_stage_manual_allowed", "route_stage_voice_allowed", "route_stage_risk_lowered",
|
||||
"unknown_policy_row", "dockerfile_one_byte", "dockerfile_legacy_unchanged",
|
||||
}:
|
||||
raise AssertionError("unknown fixture mutation")
|
||||
|
||||
@@ -1673,6 +1784,11 @@ def _build_reviewed_appops_seed_history(tmp_path, *, candidate_mutation=None):
|
||||
(working / "Dockerfile").write_bytes(candidate_dockerfile)
|
||||
|
||||
git("add", "--all")
|
||||
if candidate_mutation == "database_file_mode":
|
||||
# Windows does not preserve chmod's executable bit on the worktree;
|
||||
# stage the real Git mode mutation so this remains a meaningful tree
|
||||
# identity negative fixture on every platform.
|
||||
git("update-index", "--chmod=+x", "--", "backend/app/db/session.py")
|
||||
git("commit", "-m", "candidate source")
|
||||
candidate_revision = git("rev-parse", "HEAD")
|
||||
git("push", "origin", "HEAD:refs/heads/main")
|
||||
@@ -1729,7 +1845,7 @@ def test_appops_legacy_source_proof_accepts_the_exact_main_184_dockerfile_update
|
||||
assert proof == {
|
||||
"legacyRevision": history["oldRevision"],
|
||||
"legacyTree": history["oldTree"],
|
||||
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
|
||||
"databaseTree": "21d29c1799118fdc43946c2b6fd5585ccc2c4db1",
|
||||
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
|
||||
"candidateRevision": history["candidateRevision"],
|
||||
"composeBlob": "243aee2c6924775bed7706373a5777f4e619e053",
|
||||
@@ -1755,6 +1871,26 @@ def test_appops_legacy_source_proof_keeps_the_old_dockerfile_candidate_valid(
|
||||
assert proof["dockerfileBlob"] == "73e083dcfef25402665ace5a0bf07a083cf37432"
|
||||
|
||||
|
||||
def test_appops_legacy_source_proof_keeps_the_previous_reviewed_seed_tree_valid(
|
||||
deploy_module, monkeypatch, tmp_path
|
||||
):
|
||||
history = _build_reviewed_appops_seed_history(
|
||||
tmp_path, candidate_mutation="previous_reviewed_seed_tree"
|
||||
)
|
||||
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
|
||||
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
|
||||
|
||||
proof = deploy_module.verify_appops_legacy_source(
|
||||
history["repository"],
|
||||
_reviewed_appops_source_project(deploy_module),
|
||||
history["candidateRevision"],
|
||||
)
|
||||
|
||||
assert proof["databaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
|
||||
|
||||
|
||||
def test_appops_legacy_source_proof_rejects_a_one_byte_dockerfile_change(
|
||||
deploy_module, monkeypatch, tmp_path
|
||||
):
|
||||
@@ -1772,12 +1908,59 @@ def test_appops_legacy_source_proof_rejects_a_one_byte_dockerfile_change(
|
||||
)
|
||||
|
||||
|
||||
def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
|
||||
deploy_module, monkeypatch, tmp_path
|
||||
def test_appops_legacy_source_bindings_remain_fixed_for_the_new_reviewed_tree(
|
||||
deploy_module, monkeypatch, tmp_path,
|
||||
):
|
||||
history = _build_reviewed_appops_seed_history(tmp_path)
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
|
||||
mutations = (
|
||||
("_repository_name", "jens/other-repository"),
|
||||
("branch", "release"),
|
||||
("gitea_repository_id", 10),
|
||||
("source_dir", "/tmp/unreviewed-source"),
|
||||
("compose_project", "unreviewed-project"),
|
||||
("compose_files", ["other-compose.yml"]),
|
||||
("containers", ["OtherContainer"]),
|
||||
("verify_oci_revision", False),
|
||||
)
|
||||
for field, value in mutations:
|
||||
project = _reviewed_appops_source_project(deploy_module)
|
||||
project[field] = value
|
||||
with pytest.raises(deploy_module.DeployError, match="fixed legacy source or resolved Compose inputs"):
|
||||
deploy_module.verify_appops_legacy_source(
|
||||
history["repository"], project, history["candidateRevision"],
|
||||
)
|
||||
|
||||
for field, value in (
|
||||
("APPOPS_DATA_PATH", "/tmp/other-data"),
|
||||
("APPOPS_PUBLISH_HOST", "127.0.0.1"),
|
||||
("APPOPS_PROXY_NETWORK", "unreviewed-network"),
|
||||
):
|
||||
project = _reviewed_appops_source_project(deploy_module)
|
||||
project["environment"][field] = value
|
||||
with pytest.raises(deploy_module.DeployError, match="fixed legacy source or resolved Compose inputs"):
|
||||
deploy_module.verify_appops_legacy_source(
|
||||
history["repository"], project, history["candidateRevision"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("candidate_mutation", "expected_database_tree"),
|
||||
[
|
||||
(None, "21d29c1799118fdc43946c2b6fd5585ccc2c4db1"),
|
||||
("previous_reviewed_seed_tree", "c87c9aff5a0f0e55b9209e2f085cf1663876520a"),
|
||||
("legacy_database_unchanged", "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"),
|
||||
],
|
||||
)
|
||||
def test_appops_schema_compatibility_accepts_only_the_two_reviewed_pairs_or_same_tree(
|
||||
deploy_module, monkeypatch, tmp_path, candidate_mutation, expected_database_tree,
|
||||
):
|
||||
history = _build_reviewed_appops_seed_history(
|
||||
tmp_path, candidate_mutation=candidate_mutation,
|
||||
)
|
||||
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
|
||||
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
|
||||
assert history["candidateDatabaseTree"] == expected_database_tree
|
||||
assert history["candidateEntrypoint"] == "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
|
||||
@@ -1789,7 +1972,7 @@ def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
|
||||
assert proof == {
|
||||
"legacyRevision": history["oldRevision"],
|
||||
"legacyTree": history["oldTree"],
|
||||
"databaseTree": "c87c9aff5a0f0e55b9209e2f085cf1663876520a",
|
||||
"databaseTree": expected_database_tree,
|
||||
"entrypointBlob": "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8",
|
||||
"candidateRevision": history["candidateRevision"],
|
||||
}
|
||||
@@ -1803,6 +1986,10 @@ def test_appops_schema_compatibility_accepts_the_exact_reviewed_seed_tree_pair(
|
||||
"session_content",
|
||||
"init_db_content",
|
||||
"forge_policy_changed",
|
||||
"unknown_policy_row",
|
||||
"route_stage_manual_allowed",
|
||||
"route_stage_voice_allowed",
|
||||
"route_stage_risk_lowered",
|
||||
"database_file_mode",
|
||||
"entrypoint_content",
|
||||
],
|
||||
@@ -1815,10 +2002,13 @@ def test_appops_seed_tree_exception_rejects_every_unreviewed_tree_or_entrypoint_
|
||||
)
|
||||
assert history["oldDatabaseTree"] == "d7f8a6188f6c6e0c69bce0e547df31d36ed84a28"
|
||||
if mutation == "entrypoint_content":
|
||||
assert history["candidateDatabaseTree"] == "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
|
||||
assert history["candidateDatabaseTree"] == "21d29c1799118fdc43946c2b6fd5585ccc2c4db1"
|
||||
assert history["candidateEntrypoint"] != "4f71ff74e67d7f15e3cf6b3e79b654158dfea2f8"
|
||||
else:
|
||||
assert history["candidateDatabaseTree"] != "c87c9aff5a0f0e55b9209e2f085cf1663876520a"
|
||||
assert history["candidateDatabaseTree"] not in {
|
||||
"c87c9aff5a0f0e55b9209e2f085cf1663876520a",
|
||||
"21d29c1799118fdc43946c2b6fd5585ccc2c4db1",
|
||||
}
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_REVISION", history["oldRevision"])
|
||||
monkeypatch.setattr(deploy_module, "APPOPS_LEGACY_OLD_TREE", history["oldTree"])
|
||||
|
||||
@@ -3126,8 +3316,9 @@ def test_deployment_environment_clears_unverified_build_date(deploy_module, tmp_
|
||||
assert environment["APPOPS_BUILD_DATE"] == ""
|
||||
|
||||
|
||||
def test_deployment_environment_allows_project_build_date_override(deploy_module, tmp_path):
|
||||
def test_deployment_environment_allows_unverified_project_build_date_override(deploy_module, tmp_path):
|
||||
project = _project(tmp_path)
|
||||
project["verify_oci_revision"] = False
|
||||
project["environment"] = {"APPOPS_BUILD_DATE": "operator-configured-value"}
|
||||
repository = "jens/unraid-appops-gateway"
|
||||
revision = "a" * 40
|
||||
@@ -7573,6 +7764,8 @@ 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
|
||||
candidate_helper = "prepare_mcp_hub_candidate.py"
|
||||
assert candidate_helper in status_module._CONTROLLER_ARTIFACT_FILES
|
||||
forge_first_install_index = status_module._CONTROLLER_ARTIFACT_FILES.index(
|
||||
"forge_first_install.py"
|
||||
)
|
||||
@@ -7588,6 +7781,9 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
|
||||
else status_module.ROOT / filename
|
||||
)
|
||||
(artifact_root / filename).write_bytes(source.read_bytes())
|
||||
assert (artifact_root / candidate_helper).read_bytes() == (
|
||||
status_module.ROOT / candidate_helper
|
||||
).read_bytes()
|
||||
monkeypatch.setattr(status_module, "ROOT", artifact_root)
|
||||
monkeypatch.setattr(status_module, "CONTROLLER_RELEASE_PATH", artifact_root / "controller-release.json")
|
||||
digest = status_module._controller_artifact_digest()
|
||||
@@ -7608,7 +7804,9 @@ def test_controller_release_identity_is_content_bound(status_module, tmp_path, m
|
||||
monkeypatch.setattr(status_module.sys, "argv", ["status_server.py", "--check-artifact"])
|
||||
status_module.main()
|
||||
assert "validated controller artifact 2.0.0" in capsys.readouterr().out
|
||||
(artifact_root / "deploy.py").write_text("tampered", encoding="utf-8")
|
||||
(artifact_root / candidate_helper).write_text("tampered", encoding="utf-8")
|
||||
assert status_module._controller_release() is None
|
||||
(artifact_root / candidate_helper).unlink()
|
||||
assert status_module._controller_release() is None
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user