73 lines
2.8 KiB
Python
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")
|