Files
archy/scripts/lib/app_admin_credentials.py

73 lines
2.8 KiB
Python

#!/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")