142 lines
6.8 KiB
Python
142 lines
6.8 KiB
Python
|
|
"""Coulomb immutable recovery mirror using the proven primary multipart engine.
|
||
|
|
|
||
|
|
The adapter pins every logical request to Coulomb and remaps only to its own
|
||
|
|
isolated recovery project/bucket/prefix. Independent fetch uses the reader.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
import primary_application_archive as primary
|
||
|
|
from application_recovery_identities import (
|
||
|
|
Refused,
|
||
|
|
checkpoint,
|
||
|
|
private_directory,
|
||
|
|
private_json,
|
||
|
|
)
|
||
|
|
from application_recovery_resources import s3, status
|
||
|
|
|
||
|
|
PROJECT = "06d107bc-9d84-4672-b2a0-95701cc0c07f"
|
||
|
|
BUCKET = "railiance-coulomb-social-archives"
|
||
|
|
OPERATIONS = {"head_object", "get_object", "create_multipart_upload", "upload_part",
|
||
|
|
"complete_multipart_upload", "abort_multipart_upload"}
|
||
|
|
|
||
|
|
|
||
|
|
class Mirror:
|
||
|
|
def __init__(self, client):
|
||
|
|
self.client = client
|
||
|
|
|
||
|
|
def __getattr__(self, operation):
|
||
|
|
if operation not in OPERATIONS:
|
||
|
|
raise Refused("mirror_operation_outside_contract")
|
||
|
|
def invoke(**arguments):
|
||
|
|
if arguments.get("Bucket") != primary.BUCKET:
|
||
|
|
raise Refused("mirror_logical_bucket_refused")
|
||
|
|
key = arguments.get("Key", "")
|
||
|
|
primary.validate_key("coulomb-social", key)
|
||
|
|
return getattr(self.client, operation)(**dict(arguments, Bucket=BUCKET, Key="archives/" + key))
|
||
|
|
return invoke
|
||
|
|
|
||
|
|
|
||
|
|
def validate_receipt(receipt):
|
||
|
|
primary.validate_key("coulomb-social", receipt.get("object_key", ""))
|
||
|
|
primary.require_version(receipt.get("version_id"))
|
||
|
|
if (receipt.get("application") != "coulomb-social"
|
||
|
|
or receipt.get("schema") != "platform.application-primary-archive.v1"
|
||
|
|
or receipt.get("status") != "offsite_fetched_verified"
|
||
|
|
or receipt.get("remote_retrieval_verified") is not True
|
||
|
|
or receipt.get("destination") != "s3://" + primary.BUCKET + "/" + receipt["object_key"]
|
||
|
|
or receipt.get("immutable_create") is not True
|
||
|
|
or not re.fullmatch(r"[a-f0-9]{64}", receipt.get("ciphertext_sha256", ""))
|
||
|
|
or type(receipt.get("ciphertext_bytes")) is not int
|
||
|
|
or not 0 < receipt["ciphertext_bytes"] <= primary.MAX_BYTES):
|
||
|
|
raise Refused("verified_coulomb_primary_receipt_required")
|
||
|
|
|
||
|
|
|
||
|
|
def mirror(client, source, readback, receipt, journal, save):
|
||
|
|
validate_receipt(receipt)
|
||
|
|
binding = {k: receipt[k] for k in ("object_key", "version_id", "ciphertext_sha256", "ciphertext_bytes",
|
||
|
|
"captured_at", "image", "age_recipient", "database_sha256", "content_sha256")}
|
||
|
|
if journal:
|
||
|
|
if journal.get("primary_binding") != binding or journal.get("recovery_project_id") != PROJECT:
|
||
|
|
raise Refused("mirror_resume_binding_differs")
|
||
|
|
else:
|
||
|
|
journal.update(schema="platform.coulomb-recovery-mirror.v1", primary_binding=binding,
|
||
|
|
recovery_project_id=PROJECT, object_key=receipt["object_key"],
|
||
|
|
ciphertext_sha256=receipt["ciphertext_sha256"])
|
||
|
|
primary.regular_file(source)
|
||
|
|
if primary.digest(source) != receipt["ciphertext_sha256"] or source.stat().st_size != receipt["ciphertext_bytes"]:
|
||
|
|
raise Refused("mirror_source_differs")
|
||
|
|
primary.transfer(Mirror(client), "coulomb-social", source, readback, journal, save)
|
||
|
|
reference = {"project_id": PROJECT, "bucket": BUCKET,
|
||
|
|
"object_key": "archives/" + receipt["object_key"],
|
||
|
|
"version_id": journal["version_id"], "ciphertext_sha256": receipt["ciphertext_sha256"],
|
||
|
|
"ciphertext_bytes": receipt["ciphertext_bytes"], "hash_readback_verified": True}
|
||
|
|
journal["recovery_mirror"] = reference
|
||
|
|
save()
|
||
|
|
return dict(receipt, recovery_mirror=reference)
|
||
|
|
|
||
|
|
|
||
|
|
def retrieve(client, output, receipt):
|
||
|
|
validate_receipt(receipt)
|
||
|
|
ref = receipt.get("recovery_mirror", {})
|
||
|
|
if (ref.get("project_id") != PROJECT or ref.get("bucket") != BUCKET
|
||
|
|
or ref.get("object_key") != "archives/" + receipt["object_key"]
|
||
|
|
or ref.get("ciphertext_sha256") != receipt["ciphertext_sha256"]
|
||
|
|
or ref.get("ciphertext_bytes") != receipt["ciphertext_bytes"]
|
||
|
|
or ref.get("hash_readback_verified") is not True):
|
||
|
|
raise Refused("exact_recovery_mirror_reference_required")
|
||
|
|
primary.require_version(ref.get("version_id"))
|
||
|
|
primary.download(Mirror(client), receipt["object_key"], ref["version_id"],
|
||
|
|
receipt["ciphertext_bytes"], receipt["ciphertext_sha256"], output)
|
||
|
|
return {"status": "independent_mirror_retrieved_verified", "provider_mutations": False,
|
||
|
|
"recovery_mirror": ref, "application_restore_verified": False}
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("operation", choices=("publish", "retrieve"))
|
||
|
|
parser.add_argument("--execute", action="store_true")
|
||
|
|
parser.add_argument("--directory", type=Path, default=Path.home() / ".config/railiance-platform/coulomb-social-recovery")
|
||
|
|
parser.add_argument("--receipt", type=Path, required=True)
|
||
|
|
parser.add_argument("--source", type=Path)
|
||
|
|
parser.add_argument("--output", type=Path, required=True)
|
||
|
|
parser.add_argument("--journal", type=Path)
|
||
|
|
parser.add_argument("--result", type=Path)
|
||
|
|
args = parser.parse_args()
|
||
|
|
if not args.execute:
|
||
|
|
print(json.dumps({"status": "preview", "provider_contacted": False, "operation": args.operation}))
|
||
|
|
return
|
||
|
|
private_directory(args.directory)
|
||
|
|
private_directory(args.output.parent)
|
||
|
|
receipt = private_json(args.receipt)
|
||
|
|
if args.result:
|
||
|
|
private_directory(args.result.parent)
|
||
|
|
if args.result.exists() or args.result.is_symlink():
|
||
|
|
raise Refused("fresh_result_required")
|
||
|
|
if args.operation == "retrieve":
|
||
|
|
result = retrieve(s3(private_json(args.directory / "reader-pair.json"), PROJECT), args.output, receipt)
|
||
|
|
else:
|
||
|
|
if not args.journal or not args.source or not args.result:
|
||
|
|
raise Refused("publish_source_journal_result_required")
|
||
|
|
private_directory(args.journal.parent)
|
||
|
|
journal = private_json(args.journal) if args.journal.exists() else {}
|
||
|
|
result = mirror(s3(private_json(args.directory / "publisher-pair.json"), PROJECT), args.source,
|
||
|
|
args.output, receipt, journal, lambda: checkpoint(args.journal, journal))
|
||
|
|
if args.result:
|
||
|
|
checkpoint(args.result, result)
|
||
|
|
print(json.dumps({"status": "mirror_publish_verified" if args.operation == "publish" else result["status"],
|
||
|
|
"application_restore_verified": False, "private_values_printed": False}))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
try:
|
||
|
|
main()
|
||
|
|
except Exception as error: # noqa: BLE001 -- suppress private provider diagnostics
|
||
|
|
print(json.dumps({"status": "refused", "failure_type": type(error).__name__, "native_status": status(error)}))
|
||
|
|
sys.exit(1)
|