Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
192 lines
6.3 KiB
Python
192 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from urllib.parse import ParseResult, urlparse, urlunparse
|
|
from uuid import uuid4
|
|
|
|
from app.core.crypto import generate_install_bundle
|
|
|
|
INSTALL_SECRETS_DIR = Path("data/secrets")
|
|
INSTALL_SECRETS_FILE = INSTALL_SECRETS_DIR / "install.env"
|
|
INSTALL_SECRETS_META_FILE = INSTALL_SECRETS_DIR / "install.meta.json"
|
|
REQUIRED_KEYS = (
|
|
"POSTGRES_USER",
|
|
"POSTGRES_PASSWORD",
|
|
"POSTGRES_DB",
|
|
"DATABASE_URL",
|
|
"JWT_ACCESS_SECRET",
|
|
"JWT_REFRESH_PEPPER",
|
|
"S3_ACCESS_KEY",
|
|
"S3_SECRET_KEY",
|
|
"MINIO_ROOT_USER",
|
|
"MINIO_ROOT_PASSWORD",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class InstallSecretsStatus:
|
|
initialized: bool
|
|
locked: bool
|
|
path: str
|
|
created: bool
|
|
|
|
|
|
def _parse_env_text(raw: str) -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
for line in raw.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
values[key.strip()] = value.strip()
|
|
return values
|
|
|
|
|
|
def _render_env(values: dict[str, str]) -> str:
|
|
ordered = [f"{key}={values[key]}" for key in sorted(values.keys())]
|
|
return "\n".join(ordered) + "\n"
|
|
|
|
|
|
def read_install_secrets() -> dict[str, str]:
|
|
if not INSTALL_SECRETS_FILE.exists():
|
|
return {}
|
|
return _parse_env_text(INSTALL_SECRETS_FILE.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _write_install_secrets(values: dict[str, str]) -> None:
|
|
INSTALL_SECRETS_DIR.mkdir(parents=True, exist_ok=True)
|
|
INSTALL_SECRETS_FILE.write_text(_render_env(values), encoding="utf-8")
|
|
|
|
|
|
def _write_meta() -> None:
|
|
payload = (
|
|
"{\n"
|
|
f' "install_id": "{uuid4()}",\n'
|
|
f' "locked_at": "{datetime.now(UTC).isoformat()}"\n'
|
|
"}\n"
|
|
)
|
|
INSTALL_SECRETS_META_FILE.write_text(payload, encoding="utf-8")
|
|
|
|
|
|
def _adopt_from_environment() -> dict[str, str]:
|
|
env_values = {key: os.getenv(key, "") for key in REQUIRED_KEYS}
|
|
db_url = os.getenv("DATABASE_URL", "")
|
|
if db_url:
|
|
parsed = urlparse(db_url)
|
|
if parsed.username:
|
|
env_values["POSTGRES_USER"] = parsed.username
|
|
if parsed.password:
|
|
env_values["POSTGRES_PASSWORD"] = parsed.password
|
|
if parsed.path and parsed.path != "/":
|
|
env_values["POSTGRES_DB"] = parsed.path.lstrip("/")
|
|
return {key: value for key, value in env_values.items() if value}
|
|
|
|
|
|
def _sync_minio_s3_secrets(values: dict[str, str]) -> dict[str, str]:
|
|
"""MinIO root credentials are the S3 access key pair — keep them aligned."""
|
|
if (
|
|
values.get("S3_ACCESS_KEY") == values.get("MINIO_ROOT_USER")
|
|
and values.get("MINIO_ROOT_PASSWORD")
|
|
and values.get("S3_SECRET_KEY") != values["MINIO_ROOT_PASSWORD"]
|
|
):
|
|
values = dict(values)
|
|
values["S3_SECRET_KEY"] = values["MINIO_ROOT_PASSWORD"]
|
|
return values
|
|
|
|
|
|
def load_install_secrets_to_env() -> None:
|
|
values = _sync_minio_s3_secrets(read_install_secrets())
|
|
for key, value in values.items():
|
|
os.environ[key] = value
|
|
|
|
|
|
def ensure_install_secrets() -> InstallSecretsStatus:
|
|
existing = read_install_secrets()
|
|
if existing:
|
|
synced = _sync_minio_s3_secrets(existing)
|
|
if synced != existing:
|
|
synced["SECRETS_LOCKED"] = existing.get("SECRETS_LOCKED", "true")
|
|
_write_install_secrets(synced)
|
|
existing = synced
|
|
load_install_secrets_to_env()
|
|
return InstallSecretsStatus(True, existing.get("SECRETS_LOCKED", "false") == "true", str(INSTALL_SECRETS_FILE), False)
|
|
|
|
adopted = _adopt_from_environment()
|
|
generated = generate_install_bundle()
|
|
values = generated | adopted
|
|
values["SECRETS_LOCKED"] = "true"
|
|
_write_install_secrets(values)
|
|
_write_meta()
|
|
load_install_secrets_to_env()
|
|
return InstallSecretsStatus(True, True, str(INSTALL_SECRETS_FILE), True)
|
|
|
|
|
|
def masked_database_url(database_url: str) -> str:
|
|
parsed = urlparse(database_url)
|
|
if not parsed.username:
|
|
return database_url
|
|
password = "***" if parsed.password else ""
|
|
credentials = f"{parsed.username}:{password}" if password else parsed.username
|
|
host = parsed.hostname or ""
|
|
if parsed.port:
|
|
host = f"{host}:{parsed.port}"
|
|
netloc = f"{credentials}@{host}"
|
|
sanitized = ParseResult(
|
|
scheme=parsed.scheme,
|
|
netloc=netloc,
|
|
path=parsed.path,
|
|
params=parsed.params,
|
|
query=parsed.query,
|
|
fragment=parsed.fragment,
|
|
)
|
|
return urlunparse(sanitized)
|
|
|
|
|
|
def install_secrets_payload() -> dict:
|
|
values = read_install_secrets()
|
|
db = urlparse(values.get("DATABASE_URL", ""))
|
|
return {
|
|
"initialized": bool(values),
|
|
"locked": values.get("SECRETS_LOCKED") == "true",
|
|
"secrets_path": str(INSTALL_SECRETS_FILE),
|
|
"database": {
|
|
"host": db.hostname,
|
|
"port": db.port,
|
|
"database": db.path.lstrip("/") if db.path else "",
|
|
"user": db.username,
|
|
"password_configured": bool(values.get("POSTGRES_PASSWORD")),
|
|
},
|
|
"connection_string_masked": masked_database_url(values.get("DATABASE_URL", "")),
|
|
"secrets_status": {
|
|
"jwt_access_secret": "configured" if bool(values.get("JWT_ACCESS_SECRET")) else "missing",
|
|
"jwt_refresh_pepper": "configured" if bool(values.get("JWT_REFRESH_PEPPER")) else "missing",
|
|
"postgres_password": "configured" if bool(values.get("POSTGRES_PASSWORD")) else "missing",
|
|
"s3_secret_key": "configured" if bool(values.get("S3_SECRET_KEY")) else "missing",
|
|
"password_bcrypt_salt": "per_user_in_db",
|
|
},
|
|
}
|
|
|
|
|
|
def reveal_install_secret(key: str) -> str:
|
|
mapping = {
|
|
"database_password": "POSTGRES_PASSWORD",
|
|
"jwt_access_secret": "JWT_ACCESS_SECRET",
|
|
"jwt_refresh_pepper": "JWT_REFRESH_PEPPER",
|
|
"s3_secret_key": "S3_SECRET_KEY",
|
|
}
|
|
env_key = mapping.get(key)
|
|
if not env_key:
|
|
raise ValueError("UNSUPPORTED_SECRET_KEY")
|
|
values = read_install_secrets()
|
|
if env_key in values:
|
|
return values[env_key]
|
|
if env_key == "POSTGRES_PASSWORD":
|
|
database_url = values.get("DATABASE_URL") or os.getenv("DATABASE_URL", "")
|
|
parsed = urlparse(database_url)
|
|
return parsed.password or ""
|
|
return os.getenv(env_key, "")
|
|
|