Prepare governed S1 offsite upload
All checks were successful
CI Smoke / source-contract (push) Successful in 11s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02994-7685-7940-bf34-3555b8256018
This commit is contained in:
codex 2026-08-23 14:11:00 +02:00
parent 2c92b97f37
commit d0a5a27926
11 changed files with 844 additions and 11 deletions

View file

@ -1,13 +1,18 @@
from __future__ import annotations
import copy
import base64
import http.server
import json
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
import threading
import unittest
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest import mock
@ -26,6 +31,21 @@ from s1_backup import ( # noqa: E402
)
from s1_backup_contract import BackupContractError, load_spec, validate_spec # noqa: E402
from s1_restore import RestoreError, _safe_member, extract_bundle, validate_bundle # noqa: E402
from s1_offsite import ( # noqa: E402
OffsiteError,
contract_sha256,
plan_upload,
review_contract,
upload,
validate_prune_receipt,
)
EXPECTED_OFFSITE_MEMBERS = {
"manifest.json",
"receipt.json",
"os-config.tar.gz.age",
"packages.txt.age",
}
@unittest.skipUnless(shutil.which("age") and shutil.which("age-keygen"), "age tools required")
@ -207,6 +227,7 @@ class S1BackupRecoveryTests(unittest.TestCase):
def test_prune_requires_current_exact_approval(self) -> None:
declaration = yaml.safe_load(self.spec.read_text())
declaration["retention"]["keep_complete_bundles"] = 2
declaration["offsite"]["required_before_prune"] = False
self.spec.write_text(yaml.safe_dump(declaration, sort_keys=False))
for index in range(3):
self._create(timestamp=f"2026082{index + 1}T120000Z")
@ -219,6 +240,17 @@ class S1BackupRecoveryTests(unittest.TestCase):
self.assertEqual("pruned", result["status"])
self.assertFalse((self.output / plan["candidates"][0]).exists())
def test_prune_blocks_without_offsite_evidence(self) -> None:
declaration = yaml.safe_load(self.spec.read_text())
declaration["retention"]["keep_complete_bundles"] = 2
self.spec.write_text(yaml.safe_dump(declaration, sort_keys=False))
for index in range(3):
self._create(timestamp=f"2026082{index + 1}T120000Z")
plan = plan_prune(self.spec, self.output)
self.assertEqual("blocked-missing-offsite-evidence", plan["status"])
self.assertIsNone(plan["approval"])
self.assertEqual(["s1-backup-20260821T120000Z"], plan["missing_offsite_evidence"])
def test_status_rejects_tampered_passing_receipt(self) -> None:
bundle = self._create()
receipt_path = bundle / "receipt.json"
@ -251,6 +283,187 @@ class S1BackupRecoveryTests(unittest.TestCase):
bootstrap = (ROOT / "ansible/playbooks/bootstrap.yaml").read_text()
self.assertNotIn("s1_backup", bootstrap)
def _offsite_contract(self, base_url: str, *, accepted: bool) -> Path:
contract = yaml.safe_load((ROOT / "spec/s1-offsite.yaml").read_text())
contract["provider"]["base_url"] = base_url
contract_path = self.spec_dir / "s1-offsite.yaml"
contract_path.write_text(yaml.safe_dump(contract, sort_keys=False))
acceptance = {
"schema_version": "1.0",
"contract_sha256": contract_sha256(contract_path) if accepted else "pending",
"status": "accepted" if accepted else "pending",
"owner_repo": "railiance-platform",
"decision_id": str(uuid.uuid4()) if accepted else None,
"accepted_at": "2026-08-23T12:00:00Z" if accepted else None,
}
(self.spec_dir / "s1-offsite-owner-acceptance.yaml").write_text(
yaml.safe_dump(acceptance, sort_keys=False)
)
return contract_path
def test_offsite_contract_is_pending_owner_review(self) -> None:
review = review_contract(ROOT / "spec/s1-offsite.yaml")
self.assertEqual("review-required", review["status"])
self.assertTrue(review["approval"].startswith("APPROVE S1-OFFSITE-CONTRACT-"))
def test_deterministic_single_object_offsite_upload_and_collision(self) -> None:
token = "fixture-upload-token"
objects: dict[str, bytes] = {}
class Handler(http.server.BaseHTTPRequestHandler):
def do_PUT(self) -> None: # noqa: N802
expected_auth = "Basic " + base64.b64encode(f"{token}:".encode()).decode()
if self.headers.get("Authorization") != expected_auth:
self.send_response(401)
self.end_headers()
return
if self.headers.get("If-None-Match") != "*":
self.send_response(428)
self.end_headers()
return
if self.path in objects:
self.send_response(412)
self.end_headers()
return
length = int(self.headers["Content-Length"])
objects[self.path] = self.rfile.read(length)
self.send_response(201)
self.send_header("ETag", '"fixture-etag"')
self.end_headers()
def log_message(self, format: str, *args: object) -> None:
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
bundle = self._create()
contract = self._offsite_contract(
f"http://127.0.0.1:{server.server_port}/filesdrop", accepted=True
)
plan = plan_upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
fixture_http=True,
)
self.assertTrue(plan["executable"])
second_plan = plan_upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
fixture_http=True,
)
self.assertEqual(plan["envelope_sha256"], second_plan["envelope_sha256"])
receipt_dir = self.root / "offsite-receipts"
with mock.patch.dict(os.environ, {"RAILIANCE_BACKUP_NC_TOKEN": token}):
with self.assertRaisesRegex(OffsiteError, "exact envelope"):
upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
approval="UPLOAD-S1-OFFSITE-WRONG",
receipt_dir=receipt_dir,
fixture_http=True,
)
self.assertEqual({}, objects)
receipt_path = upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
approval=plan["approval"],
receipt_dir=receipt_dir,
fixture_http=True,
)
with self.assertRaisesRegex(OffsiteError, "collision"):
upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
approval=plan["approval"],
receipt_dir=receipt_dir,
fixture_http=True,
)
receipt = validate_prune_receipt(bundle, self.spec, receipt_path)
self.assertEqual(plan["remote_object_identity"], receipt["remote_object_identity"])
self.assertNotIn(token, receipt_path.read_text())
self.assertEqual(1, len(objects))
envelope_path = self.root / "observed-envelope.tar"
envelope_path.write_bytes(next(iter(objects.values())))
with tarfile.open(envelope_path, "r:") as archive:
self.assertEqual(EXPECTED_OFFSITE_MEMBERS, {item.name for item in archive})
finally:
server.shutdown()
server.server_close()
thread.join(timeout=2)
def test_offsite_upload_refuses_pending_owner_and_wrong_approval(self) -> None:
bundle = self._create()
contract = self._offsite_contract("http://127.0.0.1:9/filesdrop", accepted=False)
plan = plan_upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
fixture_http=True,
)
self.assertFalse(plan["executable"])
self.assertIsNone(plan["approval"])
with self.assertRaisesRegex(OffsiteError, "has not accepted"):
upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
approval="UPLOAD-S1-OFFSITE-WRONG",
receipt_dir=self.root / "receipts",
fixture_http=True,
)
def test_offsite_upload_never_follows_redirect(self) -> None:
paths: list[str] = []
class RedirectHandler(http.server.BaseHTTPRequestHandler):
def do_PUT(self) -> None: # noqa: N802
paths.append(self.path)
self.send_response(307)
self.send_header("Location", "/credential-capture")
self.end_headers()
def log_message(self, format: str, *args: object) -> None:
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RedirectHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
bundle = self._create()
contract = self._offsite_contract(
f"http://127.0.0.1:{server.server_port}/filesdrop", accepted=True
)
plan = plan_upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
fixture_http=True,
)
with mock.patch.dict(os.environ, {"RAILIANCE_BACKUP_NC_TOKEN": "redirect-token"}):
with self.assertRaisesRegex(OffsiteError, "HTTP 307"):
upload(
bundle=bundle,
backup_spec=self.spec,
contract_path=contract,
approval=plan["approval"],
receipt_dir=self.root / "redirect-receipts",
fixture_http=True,
)
self.assertEqual(1, len(paths))
self.assertNotEqual("/credential-capture", paths[0])
self.assertFalse((self.root / "redirect-receipts").exists())
finally:
server.shutdown()
server.server_close()
thread.join(timeout=2)
if __name__ == "__main__":
unittest.main()