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
@@ -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
|
||||
Reference in new issue
Block a user