email-connect/tests/test_ledger_backup.py
tegwick f77e2333af
Some checks failed
Transactional mail acceptance / mail (push) Failing after 46s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Retain independent ledger recovery custody and activate verified backups
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a11fd9-caf6-7a63-b578-9daaaa134ff2
2026-10-10 13:39:49 +02:00

485 lines
20 KiB
Python

"""Only synthetic ledger records; no production URLs, custody or SMTP sends."""
import copy
import hashlib
import importlib.util
import io
import json
import sqlite3
import subprocess
import sys
import tarfile
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from email_connect.transactional import SQLiteDeliveryStore, TransactionalApplication
@pytest.fixture
def modules(monkeypatch):
scripts = Path(__file__).resolve().parents[1] / "scripts"
monkeypatch.syspath_prepend(str(scripts))
result = []
for name in ["ledger_archive", "capture-ledger", "backup-ledger"]:
spec = importlib.util.spec_from_file_location(
name.replace("-", "_"), scripts / (name + ".py")
)
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, name.replace("-", "_"), module)
spec.loader.exec_module(module)
result.append(module)
return result
def synthetic_store(path):
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
store = SQLiteDeliveryStore(str(path))
path.chmod(0o600)
accepted = {
"registration_id": "accepted",
"normalized_email": "accepted@example.test",
"preferred_username": "synthetic",
"client_id": "synthetic-client",
"tenant": "synthetic-tenant",
"correlation_id": "trace-1",
}
expires = (datetime.now(UTC) + timedelta(days=1)).isoformat()
def reserve(payload, handle):
return store.reserve_verification(
payload, hashlib.sha256(handle.encode()).hexdigest(), expires
)[0]
request = reserve(accepted, "accepted-handle")
store.verification_accepted(request, "synthetic-provider-reference")
reserve(dict(accepted, registration_id="unknown"), "unknown-handle")
consumed = reserve(dict(accepted, registration_id="consumed"), "consumed-handle")
store.verification_accepted(consumed, "synthetic-consumed-reference")
store.consume_verification(hashlib.sha256(b"consumed-handle").hexdigest())
canceled = reserve(dict(accepted, registration_id="canceled"), "canceled-handle")
store.verification_accepted(canceled, "synthetic-canceled-reference")
store.cancel_verification(hashlib.sha256(b"canceled-handle").hexdigest())
store.reserve_send("accepted-invitation", "synthetic-accepted-fingerprint")
store.record("accepted-invitation", "synthetic-invitation-reference")
store.reserve_send("unknown-invitation", "synthetic-unknown-fingerprint")
store.suppress("suppressed@example.test", "synthetic complaint")
store.db.close()
return accepted
def captured(directory, archive, image="synthetic@sha256:fixture"):
directory.mkdir(mode=0o700)
synthetic_store(directory / "database.sqlite3")
receipt = {
"status": "captured",
"application": "email-connect",
"image": image,
"captured_at": "2026-10-07T20:00:00Z",
"elapsed_seconds": 30,
"writers_quiesced": True,
"receiver_resumed": True,
"source_pvc_read_only": True,
"database_sha256": archive.digest(directory / "database.sqlite3"),
"database_bytes": (directory / "database.sqlite3").stat().st_size,
"ledger_fingerprint": archive.fingerprint(directory / "database.sqlite3"),
}
archive.write_private(directory / "receipt.json", json.dumps(receipt).encode())
return receipt
def invoke(app, payload, path):
result = {}
raw = json.dumps(payload).encode()
environ = {
"PATH_INFO": path,
"REQUEST_METHOD": "POST",
"CONTENT_LENGTH": str(len(raw)),
"wsgi.input": io.BytesIO(raw),
"HTTP_AUTHORIZATION": "Bearer synthetic-token",
}
body = json.loads(b"".join(app(environ, lambda status, headers: result.update(status=status))))
return result["status"], body
def test_sqlite_backup_includes_committed_wal_without_source_checkpoint(modules, tmp_path):
archive, _, _ = modules
source = tmp_path / "source.sqlite3"
synthetic_store(source)
with sqlite3.connect(source) as writer:
writer.execute("PRAGMA journal_mode=WAL")
writer.execute("INSERT INTO deliveries VALUES ('wal-event','wal-reference')")
writer.commit()
original_bytes = source.read_bytes()
before = archive.fingerprint(source)
output = tmp_path / "private" / "database.sqlite3"
output.parent.mkdir(mode=0o700)
assert archive.sqlite_backup(source, output) == before
assert source.read_bytes() == original_bytes
with sqlite3.connect(output) as restored:
assert restored.execute(
"SELECT provider_ref FROM deliveries WHERE event_id='wal-event'"
).fetchone() == ("wal-reference",)
writer.execute("INSERT INTO deliveries VALUES ('uncommitted','private')")
output2 = output.parent / "another.sqlite3"
archive.sqlite_backup(source, output2)
with sqlite3.connect(output2) as restored:
assert (
restored.execute("SELECT 1 FROM deliveries WHERE event_id='uncommitted'").fetchone()
is None
)
writer.rollback()
def test_real_encryption_recovery_preserves_challenge_and_delivery_behavior(modules, tmp_path):
archive, _, _ = modules
stage = tmp_path / "stage"
stage.mkdir(mode=0o700)
receipt = captured(stage / "capture", archive)
identity = tmp_path / "synthetic-identity"
subprocess.run(["age-keygen", "-o", str(identity)], capture_output=True, check=True)
public = (
subprocess.run(["age-keygen", "-y", str(identity)], capture_output=True, check=True)
.stdout.decode()
.strip()
)
encrypted, _ = archive.seal(stage / "capture", stage, public)
first_ciphertext = encrypted.read_bytes()
assert (
archive.seal(stage / "capture", stage, "unused-retry-recipient")[0].read_bytes()
== first_ciphertext
)
transfer = dict(
receipt,
schema="platform.application-primary-archive.v1",
status="offsite_fetched_verified",
remote_retrieval_verified=True,
version_id="synthetic-version",
object_key="platform-pg/application-archives/email-connect/2026/10/07/20261007T200000Z-"
+ "a" * 24
+ ".tar.age",
destination=archive.PREFIX + "2026/10/07/20261007T200000Z-" + "a" * 24 + ".tar.age",
ciphertext_sha256=archive.digest(encrypted),
ciphertext_bytes=encrypted.stat().st_size,
)
transfer_path = tmp_path / "transfer.json"
archive.write_private(transfer_path, json.dumps(transfer).encode())
destination = tmp_path / "recovered"
result = archive.recover(encrypted, transfer_path, identity, destination)
assert result["status"] == "decrypted_ledger_verified" and not result["production_restored"]
assert not (destination / "packet.tar").exists()
assert all(p.stat().st_mode & 0o777 == 0o600 for p in destination.iterdir())
path = destination / "database.sqlite3"
before = archive.fingerprint(path)
store = SQLiteDeliveryStore(str(path))
assert archive.fingerprint(path) == before
class ForbiddenProvider:
def send(self, *args):
raise AssertionError("recovery must not resend")
app = TransactionalApplication(
store, ForbiddenProvider(), "synthetic-token", "https://issuer.example.test"
)
payload = {
"registration_id": "accepted",
"normalized_email": "accepted@example.test",
"preferred_username": "synthetic",
"client_id": "synthetic-client",
"tenant": "synthetic-tenant",
"correlation_id": "another-trace",
}
status, response = invoke(app, payload, "/v1/registration-verifications")
assert status.startswith("202") and response["accepted"]
status, response = invoke(
app, dict(payload, registration_id="unknown"), "/v1/registration-verifications"
)
assert status.startswith("422") and response["error"] == "delivery_outcome_unknown"
for handle in ["consumed-handle", "canceled-handle"]:
_, response = invoke(app, {"handle": handle}, "/v1/registration-verifications/consume")
assert response["error"] == "verification_invalid"
assert store.reference("accepted-invitation") == "synthetic-invitation-reference"
assert store.is_suppressed("suppressed@example.test")
store.db.close()
assert archive.fingerprint(path) == before
@pytest.mark.parametrize("bad", ["duplicate", "traversal", "link", "extra"])
def test_packet_refuses_all_unsafe_members_before_extract(modules, tmp_path, bad):
archive, _, _ = modules
packet = tmp_path / "packet.tar"
with tarfile.open(packet, "w") as output:
for name in ["database.sqlite3", "receipt.json"]:
entry = tarfile.TarInfo(name)
entry.size = 1
if bad == "link" and name == "database.sqlite3":
entry.type, entry.linkname, entry.size = tarfile.SYMTYPE, "/tmp/private", 0
output.addfile(entry, io.BytesIO(b"x"))
if bad != "link":
entry = tarfile.TarInfo(
{"duplicate": "database.sqlite3", "traversal": "../escape", "extra": "smtp.env"}[
bad
]
)
entry.size = 1
output.addfile(entry, io.BytesIO(b"x"))
destination = tmp_path / "out"
destination.mkdir(mode=0o700)
with pytest.raises(archive.Refused, match="invalid_packet_members"):
archive.unpack(packet, destination)
assert not list(destination.iterdir())
def deployment(capture):
return {
"spec": {"replicas": 1, "template": {"spec": {"containers": [{"image": capture.IMAGE}]}}},
"status": {"readyReplicas": 1},
}
@pytest.mark.parametrize(
"failure", [None, "capture", "interrupt", "helper-cleanup", "foreign-image"]
)
def test_capture_resumes_unchanged_receiver_after_failures(modules, monkeypatch, tmp_path, failure):
archive, capture, _ = modules
before = deployment(capture)
current = copy.deepcopy(before)
calls = []
def k(*args, **kwargs):
calls.append(args)
if args[0] == "patch":
patches = json.loads(kwargs["body"])
assert patches[0]["value"] == before["spec"]["template"]
assert patches[1]["value"] == current["spec"]["replicas"]
current["spec"]["replicas"] = patches[2]["value"]
if args[0] == "delete" and failure == "helper-cleanup":
raise capture.Refused("cleanup failed")
return b""
def document(*args, **kwargs):
return {"items": []} if args[1] == "pods" else copy.deepcopy(current)
def capture_database(name, destination):
if failure == "capture":
raise capture.Refused("capture failed")
if failure == "interrupt":
raise KeyboardInterrupt()
if failure == "foreign-image":
current["spec"]["template"]["spec"]["containers"][0]["image"] = "foreign-image"
synthetic_store(destination / "database.sqlite3")
path = destination / "database.sqlite3"
return {
"database_sha256": archive.digest(path),
"database_bytes": path.stat().st_size,
"ledger_fingerprint": archive.fingerprint(path),
"writers_quiesced": True,
"source_pvc_read_only": True,
}
monkeypatch.setattr(capture, "k", k)
monkeypatch.setattr(capture, "document", document)
monkeypatch.setattr(capture, "capture_database", capture_database)
destination = tmp_path / "capture"
body = json.dumps(capture.helper("synthetic-capture")).encode()
if failure:
with pytest.raises((archive.Refused, KeyboardInterrupt)):
capture.capture(before, body, destination)
else:
assert capture.capture(before, body, destination)["status"] == "captured"
receipt = json.loads((destination / "receipt.json").read_text())
assert any(args[0] == "delete" for args in calls)
if failure == "foreign-image":
assert current["spec"]["replicas"] == 0 and receipt["receiver_state_requires_check"]
assert not receipt.get("receiver_resumed")
else:
assert current == before and receipt["receiver_resumed"]
assert receipt["status"] == ("captured" if failure is None else "failed")
@pytest.mark.parametrize("failure", [None, "publish", "encrypt", "mirror"])
def test_backup_success_requires_readback_and_preserves_one_pending_capture(
modules, monkeypatch, tmp_path, failure
):
archive, _, runner = modules
captures, publications = [], []
original_seal = runner.seal
state = tmp_path / "state"
def command(argv, **kwargs):
if "--primary-receipt" in argv:
if failure == "mirror":
raise archive.Refused("synthetic mirror failure")
archive.checkpoint(Path(argv[-1]), {"hash_readback_verified": True})
return
captures.append(argv)
captured(Path(argv[-1]), archive)
def seal(directory, stage, recipient):
if failure == "encrypt":
raise archive.Refused("synthetic encryption failure")
return original_seal(directory, stage, recipient)
def publish(encrypted, fetched, receipt_path, capture, tool, python):
publications.append(encrypted.read_bytes())
if failure == "publish":
raise archive.Refused("synthetic readback failure")
archive.write_private(fetched, encrypted.read_bytes())
key = (
"platform-pg/application-archives/email-connect/2026/10/07/20261007T200000Z-"
+ "b" * 24
+ ".tar.age"
)
return {
"schema": "platform.application-primary-archive.v1",
"application": "email-connect",
"status": "offsite_fetched_verified",
"remote_retrieval_verified": True,
"version_id": "synthetic-version",
"object_key": key,
"destination": archive.PREFIX + key.rsplit("/", 1)[-1],
"captured_at": capture["captured_at"],
"ciphertext_sha256": archive.digest(encrypted),
"ciphertext_bytes": encrypted.stat().st_size,
"immutable_create": True,
}
identity = tmp_path / "synthetic-identity"
subprocess.run(["age-keygen", "-o", str(identity)], capture_output=True, check=True)
public = (
subprocess.run(["age-keygen", "-y", str(identity)], capture_output=True, check=True)
.stdout.decode()
.strip()
)
monkeypatch.setattr(runner, "command", command)
monkeypatch.setattr(runner, "seal", seal)
monkeypatch.setattr(runner, "publish", publish)
if failure:
with pytest.raises(archive.Refused):
runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public,
mirror_required=failure == "mirror")
assert not (state / "last-success.json").exists()
assert len(list(state.glob("pending-*"))) == 1
with pytest.raises(archive.Refused, match="pending_backup_requires_review"):
runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public)
if failure in {"publish", "mirror"}:
mirror_required = failure == "mirror"
ciphertext = publications[0]
failure = None
result = runner.backup(
state, Path("synthetic-tool"), Path("synthetic-python"), public, resume=True,
mirror_required=mirror_required,
)
assert len(captures) == 1 and publications == [ciphertext, ciphertext]
assert result["application_restore_verified"] is False
else:
result = runner.backup(state, Path("synthetic-tool"), Path("synthetic-python"), public)
assert not list(state.glob("pending-*"))
assert not list(state.rglob("database.sqlite3"))
assert json.loads((state / "inventory.json").read_text()) == [result]
assert runner.freshness(state, datetime(2026, 10, 8, 20, tzinfo=UTC))["status"] == "fresh"
with pytest.raises(archive.Refused, match="backup_stale"):
runner.freshness(state, datetime(2026, 10, 9, 20, tzinfo=UTC))
(state / result["local_archive"]).write_bytes(b"corruption")
with pytest.raises(archive.Refused, match="local_ciphertext_mismatch"):
runner.freshness(state, datetime(2026, 10, 8, 20, tzinfo=UTC))
@pytest.mark.parametrize(
"field,value",
[
("receiver_resumed", False),
("database_sha256", "bad"),
("ledger_fingerprint", {}),
("source_pvc_read_only", False),
],
)
def test_invalid_capture_cannot_be_sealed(modules, tmp_path, field, value):
archive, _, _ = modules
stage = tmp_path / "stage"
stage.mkdir(mode=0o700)
receipt = captured(stage / "capture", archive)
receipt[field] = value
(stage / "capture" / "receipt.json").write_text(json.dumps(receipt))
with pytest.raises(archive.Refused):
archive.seal(stage / "capture", stage, "unused public recipient")
assert not (stage / "packet.tar").exists()
def test_helper_has_no_secret_or_token_mount_and_requires_no_egress(modules):
_, capture, _ = modules
pod = capture.helper("synthetic-helper")
container = pod["spec"]["containers"][0]
assert not pod["spec"]["automountServiceAccountToken"]
assert "env" not in container and "envFrom" not in container
assert container["volumeMounts"][0]["readOnly"]
labels = pod["metadata"]["labels"]
deny = {"spec": {"policyTypes": ["Ingress", "Egress"], "podSelector": {}}}
scoped = {
"spec": {
"policyTypes": ["Egress"],
"podSelector": {"matchLabels": {"app.kubernetes.io/name": "email-connect"}},
"egress": [{}],
}
}
capture.require_no_helper_egress([deny, scoped], labels)
with pytest.raises(capture.Refused, match="helper_egress_not_denied"):
capture.require_no_helper_egress([], labels)
broad = copy.deepcopy(scoped)
broad["spec"]["podSelector"] = {}
with pytest.raises(capture.Refused, match="helper_egress_not_denied"):
capture.require_no_helper_egress([deny, broad], labels)
assert capture.selected(
{"matchExpressions": [{"key": "email-ledger-capture", "operator": "Exists"}]}, labels
)
assert not capture.selected(
{
"matchExpressions": [
{"key": "email-ledger-capture", "operator": "In", "values": ["other"]}
]
},
labels,
)
def test_scoped_ledger_rehearsal_preserves_ownership_and_budgets(modules, tmp_path, monkeypatch, capsys):
archive, _, _ = modules
path = tmp_path / "source.sqlite3"
synthetic_store(path)
store = SQLiteDeliveryStore(str(path))
payload = {
"registration_id": "scoped", "normalized_email": "scoped@example.test",
"preferred_username": "synthetic", "client_id": "synthetic-client",
"tenant": "synthetic-tenant", "correlation_id": "trace-scoped",
}
expires = (datetime.now(UTC) + timedelta(days=1)).isoformat()
request, _ = store.reserve_verification(
payload, hashlib.sha256(b"scoped-handle").hexdigest(), expires, caller_id="caller-a"
)
store.verification_accepted(request, "synthetic-scoped-reference")
store.reserve_verification(
dict(payload, registration_id="scoped-unknown"),
hashlib.sha256(b"scoped-unknown-handle").hexdigest(), expires, caller_id="caller-b"
)
assert store.request_budget("caller-a", rate=2, burst=1, now=100)[0]
store.db.close()
before = archive.fingerprint(path)
assert before["tables"]["caller_buckets"]["rows"] == 1
script = Path(__file__).resolve().parents[1] / "scripts/rehearse-ledger.py"
spec = importlib.util.spec_from_file_location("rehearsal", script)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
recovered = tmp_path / "recovered.sqlite3"
code = module.CODE.replace("Path('/tmp/recovered.sqlite3')", "Path(" + repr(str(recovered)) + ")")
monkeypatch.setattr(sys, "stdin", io.TextIOWrapper(io.BytesIO(path.read_bytes())))
monkeypatch.setattr(__import__("os"), "getuid", lambda: 10001)
exec(compile(code, str(script), "exec"), {})
receipt = json.loads(capsys.readouterr().out)
assert receipt["checks"]["accepted_verifications"] == 4
assert receipt["checks"]["unknown_verifications"] == 2
assert receipt["mail_sent"] is False
assert archive.fingerprint(recovered) == before
restarted = SQLiteDeliveryStore(str(recovered))
assert not restarted.request_budget("caller-a", rate=2, burst=1, now=100)[0]
restarted.db.close()