48 lines
2.4 KiB
Python
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)
|