# services/keycloak/bootstrap-jobs/user-overrides-job.yaml
# One-off job for sso/keycloak-user-overrides-9.
# Purpose: keycloak user overrides 9 (see container args/env in this file).
# Run by setting spec.suspend to false, reconcile, then set it back to true.
# Safe to delete the finished Job/pod; it should not run continuously.
apiVersion: batch/v1
kind: Job
metadata:
  name: keycloak-user-overrides-9
  namespace: sso
spec:
  ttlSecondsAfterFinished: 3600
  suspend: true
  backoffLimit: 0
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-pre-populate-only: "true"
        vault.hashicorp.com/role: "sso"
        vault.hashicorp.com/agent-inject-secret-keycloak-env.sh: "kv/data/atlas/shared/keycloak-admin"
        vault.hashicorp.com/agent-inject-template-keycloak-env.sh: |
          {{ with secret "kv/data/atlas/shared/keycloak-admin" }}
          export KEYCLOAK_ADMIN="{{ .Data.data.username }}"
          export KEYCLOAK_ADMIN_USER="{{ .Data.data.username }}"
          export KEYCLOAK_ADMIN_PASSWORD="{{ .Data.data.password }}"
          {{ end }}
          {{ with secret "kv/data/atlas/sso/keycloak-db" }}
          export KC_DB_URL_DATABASE="{{ .Data.data.POSTGRES_DATABASE }}"
          export KC_DB_USERNAME="{{ .Data.data.POSTGRES_USER }}"
          export KC_DB_PASSWORD="{{ .Data.data.POSTGRES_PASSWORD }}"
          {{ end }}
          {{ with secret "kv/data/atlas/shared/portal-e2e-client" }}
          export PORTAL_E2E_CLIENT_ID="{{ .Data.data.client_id }}"
          export PORTAL_E2E_CLIENT_SECRET="{{ .Data.data.client_secret }}"
          {{ end }}
          {{ with secret "kv/data/atlas/sso/openldap-admin" }}
          export LDAP_ADMIN_PASSWORD="{{ .Data.data.LDAP_ADMIN_PASSWORD }}"
          export LDAP_CONFIG_PASSWORD="{{ .Data.data.LDAP_CONFIG_PASSWORD }}"
          export LDAP_BIND_PASSWORD="${LDAP_ADMIN_PASSWORD}"
          {{ end }}
          {{ with secret "kv/data/atlas/shared/postmark-relay" }}
          export KEYCLOAK_SMTP_USER="{{ index .Data.data "apikey" }}"
          export KEYCLOAK_SMTP_PASSWORD="{{ index .Data.data "apikey" }}"
          {{ end }}
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: hardware
                    operator: In
                    values: ["rpi5", "rpi4"]
                  - key: node-role.kubernetes.io/worker
                    operator: Exists
      restartPolicy: Never
      serviceAccountName: sso-vault
      containers:
        - name: configure
          image: python:3.11-alpine
          env:
            - name: KEYCLOAK_SERVER
              value: http://keycloak.sso.svc.cluster.local
            - name: KEYCLOAK_REALM
              value: atlas
            - name: OVERRIDE_USERNAME
              value: bstein
            - name: OVERRIDE_MAILU_EMAIL
              value: brad@bstein.dev
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -euo pipefail
              . /vault/secrets/keycloak-env.sh
              python - <<'PY'
              import json
              import os
              import urllib.parse
              import urllib.error
              import urllib.request

              base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/")
              realm = os.environ["KEYCLOAK_REALM"]
              admin_user = os.environ["KEYCLOAK_ADMIN_USER"]
              admin_password = os.environ["KEYCLOAK_ADMIN_PASSWORD"]

              override_username = os.environ["OVERRIDE_USERNAME"].strip()
              override_mailu_email = os.environ["OVERRIDE_MAILU_EMAIL"].strip()
              if not override_username or not override_mailu_email:
                  raise SystemExit("Missing override inputs")

              def http_json(method: str, url: str, token: str, payload=None):
                  data = None
                  headers = {"Authorization": f"Bearer {token}"}
                  if payload is not None:
                      data = json.dumps(payload).encode()
                      headers["Content-Type"] = "application/json"
                  req = urllib.request.Request(url, data=data, headers=headers, method=method)
                  try:
                      with urllib.request.urlopen(req, timeout=30) as resp:
                          body = resp.read()
                          if not body:
                              return resp.status, None
                          return resp.status, json.loads(body.decode())
                  except urllib.error.HTTPError as exc:
                      raw = exc.read()
                      if not raw:
                          return exc.code, None
                      try:
                          return exc.code, json.loads(raw.decode())
                      except Exception:
                          return exc.code, {"raw": raw.decode(errors="replace")}

              token_data = urllib.parse.urlencode(
                  {
                      "grant_type": "password",
                      "client_id": "admin-cli",
                      "username": admin_user,
                      "password": admin_password,
                  }
              ).encode()
              token_req = urllib.request.Request(
                  f"{base_url}/realms/master/protocol/openid-connect/token",
                  data=token_data,
                  headers={"Content-Type": "application/x-www-form-urlencoded"},
                  method="POST",
              )
              try:
                  with urllib.request.urlopen(token_req, timeout=10) as resp:
                      token_body = json.loads(resp.read().decode())
              except urllib.error.HTTPError as exc:
                  body = exc.read().decode(errors="replace")
                  raise SystemExit(f"Token request failed: status={exc.code} body={body}")
              access_token = token_body["access_token"]

              # Find target user id.
              status, users = http_json(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/users?username={urllib.parse.quote(override_username)}&exact=true&max=1",
                  access_token,
              )
              if status != 200 or not isinstance(users, list) or not users:
                  raise SystemExit(f"User not found: {override_username}")
              user = users[0] if isinstance(users[0], dict) else None
              user_id = (user or {}).get("id") or ""
              if not user_id:
                  raise SystemExit("User id missing")

              # Fetch full user and update only attributes.
              status, full = http_json("GET", f"{base_url}/admin/realms/{realm}/users/{user_id}", access_token)
              if status != 200 or not isinstance(full, dict):
                  raise SystemExit("Unable to fetch user")

              attrs = full.get("attributes") or {}
              if not isinstance(attrs, dict):
                  attrs = {}
              existing = attrs.get("mailu_email")
              needs_update = True
              if isinstance(existing, list) and existing and existing[0] == override_mailu_email:
                  needs_update = False
              if isinstance(existing, str) and existing == override_mailu_email:
                  needs_update = False

              if needs_update:
                  attrs["mailu_email"] = [override_mailu_email]
                  status, _ = http_json(
                      "PUT",
                      f"{base_url}/admin/realms/{realm}/users/{user_id}",
                      access_token,
                      {"attributes": attrs},
                  )
                  if status not in (200, 204):
                      raise SystemExit(f"Unexpected user update response: {status}")

              # Ensure the user is in the admin and planka-users groups.
              def ensure_group(group_name: str) -> None:
                  status, groups = http_json(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(group_name)}",
                      access_token,
                  )
                  if status != 200 or not isinstance(groups, list):
                      raise SystemExit("Unable to fetch groups")
                  group_id = ""
                  for item in groups:
                      if isinstance(item, dict) and item.get("name") == group_name:
                          group_id = item.get("id") or ""
                          break
                  if not group_id:
                      raise SystemExit(f"{group_name} group not found")
                  status, memberships = http_json(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/users/{user_id}/groups",
                      access_token,
                  )
                  if status != 200 or not isinstance(memberships, list):
                      raise SystemExit("Unable to read user groups")
                  already = any(
                      isinstance(item, dict) and item.get("id") == group_id for item in memberships
                  )
                  if already:
                      return
                  status, _ = http_json(
                      "PUT",
                      f"{base_url}/admin/realms/{realm}/users/{user_id}/groups/{group_id}",
                      access_token,
                  )
                  if status not in (200, 204):
                      raise SystemExit(
                          f"Unexpected group update response for {group_name}: {status}"
                      )

              for group in ("admin", "planka-users"):
                  ensure_group(group)
              PY
          volumeMounts:
