Bind essentials recovery to explicit manifest and verified offsite provenance
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a06ecb-456a-71c2-b41e-0755d336e883
This commit is contained in:
parent
4707bf08d7
commit
11322a5256
7 changed files with 151 additions and 14 deletions
|
|
@ -22,8 +22,14 @@ def file_digest(path):
|
|||
with path.open('rb') as f: return hashlib.file_digest(f,'sha256').hexdigest()
|
||||
|
||||
|
||||
def run(archive, receipt):
|
||||
validate_archive(archive)
|
||||
def run(archive, receipt, profile="full"):
|
||||
validate_archive(archive,profile)
|
||||
if profile == "essentials":
|
||||
with zipfile.ZipFile(archive) as bundle:
|
||||
manifest=json.loads(bundle.read("backup-manifest.json"))
|
||||
if manifest.get("profile")!="essentials" or manifest.get("package_registry_available") is not False:
|
||||
raise ValueError("essentials_contract_required")
|
||||
receipt["archive_profile"]=profile
|
||||
prefix='wp0029-'+secrets.token_hex(5)
|
||||
network=prefix+'-net'; db=prefix+'-db'; app=prefix+'-app'
|
||||
staging=archive.parent/(prefix+'-staging')
|
||||
|
|
@ -90,6 +96,8 @@ DISABLE_REGISTRATION = true
|
|||
REQUIRE_SIGNIN_VIEW = false
|
||||
[mailer]
|
||||
ENABLED = false
|
||||
[packages]
|
||||
ENABLED = {"false" if profile == "essentials" else "true"}
|
||||
[actions]
|
||||
ENABLED = false
|
||||
[webhook]
|
||||
|
|
@ -150,6 +158,12 @@ LEVEL = Error
|
|||
docker('exec','--user','1000:1000',app,'git','clone','--quiet','http://127.0.0.1:3000/'+repo+'.git',target)
|
||||
docker('exec','--user','1000:1000',app,'git','-C',target,'fsck','--full')
|
||||
receipt['repositories_verified'].append(repo)
|
||||
if profile == 'essentials':
|
||||
if any(p.is_file() for p in (staging/'data'/'packages').rglob('*')):
|
||||
raise ValueError('unexpected_package_payload')
|
||||
receipt.update(status='restored_essentials',package_registry_available=False,
|
||||
primary_storage_accessed=False)
|
||||
return
|
||||
receipt['stage']='package_blob_recovery'
|
||||
# Each stored blob must survive extraction and match its database digest.
|
||||
rows=docker('exec',db,'psql','-U','forgejo','-d','forgejo','-Atqc','SELECT hash_sha256 FROM package_blob;').stdout.decode().splitlines()
|
||||
|
|
@ -173,21 +187,29 @@ LEVEL = Error
|
|||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--archive',required=True,type=Path);p.add_argument('--receipt',required=True)
|
||||
p.add_argument('--profile',choices=['full','essentials'],default='full')
|
||||
p.add_argument('--transfer-receipt',required=True,type=Path)
|
||||
a=p.parse_args();receipt={'schema':'platform.forgejo-isolated-restore.v1','status':'failed','images':[FORGE,POSTGRES]}
|
||||
fd=os.open(a.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
||||
try:
|
||||
transfer=json.loads(a.transfer_receipt.read_text())
|
||||
if (transfer.get('status') != 'offsite_fetched_pending_isolated_restore'
|
||||
or transfer.get('download_http_status') != 200
|
||||
nextcloud = (transfer.get('status') == 'offsite_fetched_pending_isolated_restore'
|
||||
and transfer.get('download_http_status') == 200)
|
||||
primary = (transfer.get('status') == 'primary_fetched_pending_application_restore'
|
||||
and transfer.get('download_hash_matches') is True and transfer.get('decrypted') is True)
|
||||
if (not (nextcloud or primary)
|
||||
or transfer.get('plaintext_sha256') != file_digest(a.archive)):
|
||||
raise ValueError('verified_offsite_provenance_required')
|
||||
receipt['offsite_artifact']=transfer['artifact']
|
||||
receipt['source_provider']='Scaleway' if primary else 'Nextcloud'
|
||||
receipt['offsite_artifact']=transfer['destination'] if primary else transfer['artifact']
|
||||
receipt['ciphertext_sha256']=transfer['ciphertext_sha256']
|
||||
run(a.archive,receipt)
|
||||
except Exception: receipt['error']='isolated_restore_failed'
|
||||
if transfer.get('archive_profile','full')!=a.profile: raise ValueError('profile_provenance_mismatch')
|
||||
run(a.archive,receipt,a.profile)
|
||||
except Exception:
|
||||
receipt['status']='failed'
|
||||
receipt['error']='isolated_restore_failed'
|
||||
finally:
|
||||
with os.fdopen(fd,'w') as f: json.dump(receipt,f,indent=2)
|
||||
return int(receipt['status']!='restored' or not receipt.get('cleanup'))
|
||||
return int(receipt['status'] not in ('restored','restored_essentials') or not receipt.get('cleanup'))
|
||||
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue