#!/usr/bin/env python3 """Legacy installer counterpart of container::app_admin::initial_password. Only fresh empty application storage may receive a new bootstrap credential. Existing accounts must use the freshly authorized native API migration RPC. """ import argparse import os from pathlib import Path import secrets import stat import tempfile def read_private(path): fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) with os.fdopen(fd, "r") as source: metadata = os.fstat(source.fileno()) if not stat.S_ISREG(metadata.st_mode) or metadata.st_mode & 0o077: raise ValueError("Requires private app credential storage") password = source.read(8193).strip() if not 16 <= len(password) <= 8192 or "${" in password: raise ValueError("Requires app administrator password rotation") return password def initial_password(root, data, app): root, data = Path(root), Path(data) if app not in ("photoprism", "grafana"): raise ValueError("Unsupported administrator app") if (root / f"{app}-admin-rotation.json").exists(): raise ValueError("Requires completion of pending app administrator rotation") path = root / f"{app}-admin-password" if os.path.lexists(path): return read_private(path) if data.exists() and any(data.iterdir()): raise ValueError("Requires app administrator migration; existing accounts and data preserved") root.mkdir(parents=True, exist_ok=True, mode=0o700) password = secrets.token_hex(32) descriptor, temporary = tempfile.mkstemp(prefix=".app-admin-", dir=root) try: with os.fdopen(descriptor, "w") as output: if os.geteuid() == 0: metadata = root.stat() os.fchown(output.fileno(), metadata.st_uid, metadata.st_gid) output.write(password) output.flush() os.fsync(output.fileno()) try: os.link(temporary, path) except FileExistsError: return read_private(path) directory = os.open(root, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(directory) finally: os.close(directory) return password finally: os.unlink(temporary) if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("app", choices=("photoprism", "grafana")) parser.add_argument("--secrets-dir", default="/var/lib/archipelago/secrets") parser.add_argument("--data-root", default="/var/lib/archipelago") args = parser.parse_args() try: print(initial_password(args.secrets_dir, Path(args.data_root) / args.app, args.app)) except (OSError, ValueError) as error: parser.exit(1, f"App credential preparation refused: {error}\n")