Files
werkjournal/scripts/admin-recovery-remote.py
2026-09-09 13:53:51 +02:00

48 lines
2.4 KiB
Python

"""Explicit offline recovery; Deployment helpers are injected by the SSH wrapper."""
import os
import re
import subprocess
def prepare_recovery(deployment):
deployment.preflight()
if deployment.pending.exists() or deployment.flag.exists():
raise RuntimeError('Resolve existing deployment/maintenance before administrator recovery')
expected = manifest(deployment.app / 'current')
folder = deployment.home / '.config/werkjournal'
if folder.parent.is_symlink() or folder.is_symlink():
raise RuntimeError('Recovery directory must not be a symlink')
folder.mkdir(mode=0o700, parents=True, exist_ok=True)
if folder.stat().st_mode & 0o077:
raise RuntimeError('Recovery directory must be private to its owner')
destination = folder / 'admin-recovery-code'
temporary = destination.with_name(destination.name + '.next')
if destination.is_symlink() or temporary.exists() or temporary.is_symlink():
raise RuntimeError('Unexpected recovery file layout')
deployment.maintenance()
deployment.stop()
try:
# Discard JVM diagnostics: neither exception output nor process arguments contain the code.
result = subprocess.run([str(deployment.home / 'opt/jdk25/bin/java'), '-Xmx128m',
'-jar', str(deployment.app / 'current/werkjournal.jar'),
'--prepare-admin-recovery'], cwd=deployment.app,
env={'HOME': str(deployment.home), 'PATH': '/usr/bin:/bin'},
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, timeout=60)
if result.returncode != 0 or re.fullmatch(r'[A-Za-z0-9_-]{43}', result.stdout) is None:
raise RuntimeError('Recovery refused or failed; an administrator may still exist')
atomic(destination, result.stdout + '\n')
finally:
deployment.start()
deployment.ready(expected)
deployment.flag.unlink()
sync_directory(deployment.site)
print('Recovery prepared for 24 hours. Read ~/.config/werkjournal/admin-recovery-code privately; enter it on the login setup form, then verify the email.')
if __name__ == '__main__':
os.umask(0o077)
deployment = Deployment(Path.home())
with (deployment.app / '.deploy.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
prepare_recovery(deployment)