Files
Unraid-AppOps-Gateway-Public/backend/app/services/repopulse_secret_import.py
T

89 lines
2.7 KiB
Python

"""One-time, write-only import of RepoPulse's existing Gitea credential.
Run inside the AppOps container as its service account. The credential is read
from stdin so it never appears in argv, environment, logs, or command output.
"""
from __future__ import annotations
import hmac
import json
import sys
from sqlmodel import Session
from ..db.session import engine
from .secret_store import get_secret, has_secret, secret_storage_status, set_secret
SECRET_REF = "REPOPULSE_GITEA_TOKEN"
BOOTSTRAP_SECRET_REF = "ENROLLMENT_BOOTSTRAP"
MAX_SECRET_BYTES = 8192
def _import_fixed_secret(
session: Session, token: str, *, secret_ref: str, used_by: str, purpose: str
) -> str:
if not secret_storage_status()["productionReady"]:
raise ValueError("production secret storage is not ready")
if not token or token != token.strip() or "\n" in token or "\r" in token:
raise ValueError("invalid credential input")
if len(token.encode("utf-8")) > MAX_SECRET_BYTES:
raise ValueError("credential input too large")
existing = get_secret(session, secret_ref, touch=False)
if existing is None and has_secret(session, secret_ref):
raise ValueError("credential reference cannot be decrypted")
if existing is not None:
if hmac.compare_digest(existing, token):
return "already_present"
raise ValueError("credential reference already contains a different value")
set_secret(
session,
secret_ref,
token,
used_by=[used_by],
metadata={"purpose": purpose},
)
return "imported"
def import_repopulse_gitea_token(session: Session, token: str) -> str:
return _import_fixed_secret(
session,
token,
secret_ref=SECRET_REF,
used_by="deployment:repopulse",
purpose="repopulse_gitea_access",
)
def import_controller_bootstrap(session: Session, token: str) -> str:
return _import_fixed_secret(
session,
token,
secret_ref=BOOTSTRAP_SECRET_REF,
used_by="deployment:enrollment_controller",
purpose="controller_enrollment_bootstrap",
)
def main() -> int:
raw = sys.stdin.buffer.read(MAX_SECRET_BYTES + 1)
if len(raw) > MAX_SECRET_BYTES:
print(json.dumps({"status": "blocked", "reason": "credential input too large"}))
return 2
try:
token = raw.decode("utf-8")
with Session(engine) as session:
outcome = import_repopulse_gitea_token(session, token)
except (UnicodeDecodeError, ValueError):
print(json.dumps({"status": "blocked", "reason": "credential import rejected"}))
return 2
print(json.dumps({"status": outcome, "secretRef": SECRET_REF}))
return 0
if __name__ == "__main__":
raise SystemExit(main())