# services/keycloak/oneoffs/veles-realm-ensure-job.yaml
# One-off job for sso/veles-realm-ensure-4.
# Purpose: create the Veles realm, groups, OIDC client, SMTP settings, and Vault client secret.
# Keep suspended until Veles Vault paths/policies have reconciled, then unsuspend once.
apiVersion: batch/v1
kind: Job
metadata:
  name: veles-realm-ensure-4
  namespace: sso
spec:
  suspend: true
  backoffLimit: 0
  ttlSecondsAfterFinished: 3600
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/agent-pre-populate-only: "true"
        vault.hashicorp.com/role: "sso-secrets"
        vault.hashicorp.com/agent-inject-secret-keycloak-admin-env.sh: "kv/data/atlas/shared/keycloak-admin"
        vault.hashicorp.com/agent-inject-template-keycloak-admin-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/shared/postmark-relay" }}
          export KEYCLOAK_SMTP_USER="{{ index .Data.data "apikey" }}"
          export KEYCLOAK_SMTP_PASSWORD="{{ index .Data.data "apikey" }}"
          {{ end }}
    spec:
      serviceAccountName: mas-secrets-ensure
      restartPolicy: Never
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: node-role.kubernetes.io/worker
                    operator: Exists
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - key: kubernetes.io/arch
                    operator: In
                    values: ["arm64"]
      containers:
        - name: configure
          image: python:3.11-alpine
          env:
            - name: KEYCLOAK_SERVER
              value: http://keycloak.sso.svc.cluster.local
            - name: KEYCLOAK_REALM
              value: veles
            - name: KEYCLOAK_CLIENT_ID
              value: veles-web
            - name: KEYCLOAK_PUBLIC_ISSUER
              value: https://sso.bstein.dev/realms/veles
            - name: VELES_BASE_URL
              value: https://veles.bstein.dev
            - name: KEYCLOAK_SMTP_HOST
              value: mail.bstein.dev
            - name: KEYCLOAK_SMTP_PORT
              value: "587"
            - name: KEYCLOAK_SMTP_FROM
              value: no-reply-veles@bstein.dev
            - name: KEYCLOAK_SMTP_FROM_NAME
              value: Veles
            - name: VELES_PUBLIC_REPLAY_USERS
              value: veles-dev,daniel-test,viktor-test
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -eu
              . /vault/secrets/keycloak-admin-env.sh
              python - <<'PY'
              import json
              import os
              import time
              import urllib.error
              import urllib.parse
              import urllib.request

              base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/")
              realm = os.environ["KEYCLOAK_REALM"]
              client_id = os.environ["KEYCLOAK_CLIENT_ID"]
              issuer = os.environ["KEYCLOAK_PUBLIC_ISSUER"]
              veles_base_url = os.environ["VELES_BASE_URL"].rstrip("/")
              admin_user = os.environ["KEYCLOAK_ADMIN_USER"]
              admin_password = os.environ["KEYCLOAK_ADMIN_PASSWORD"]

              def request(method, url, token=None, payload=None, headers=None, timeout=30):
                  data = None
                  req_headers = headers.copy() if headers else {}
                  if token:
                      req_headers["Authorization"] = f"Bearer {token}"
                  if payload is not None:
                      data = json.dumps(payload).encode()
                      req_headers["Content-Type"] = "application/json"
                  req = urllib.request.Request(url, data=data, headers=req_headers, method=method)
                  try:
                      with urllib.request.urlopen(req, timeout=timeout) 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_body = None
              form = urllib.parse.urlencode(
                  {
                      "grant_type": "password",
                      "client_id": "admin-cli",
                      "username": admin_user,
                      "password": admin_password,
                  }
              ).encode()
              for attempt in range(1, 11):
                  req = urllib.request.Request(
                      f"{base_url}/realms/master/protocol/openid-connect/token",
                      data=form,
                      headers={"Content-Type": "application/x-www-form-urlencoded"},
                      method="POST",
                  )
                  try:
                      with urllib.request.urlopen(req, timeout=10) as resp:
                          token_body = json.loads(resp.read().decode())
                          break
                  except urllib.error.URLError as exc:
                      if attempt == 10:
                          raise SystemExit(f"Keycloak token request failed after retries: {exc}")
                      time.sleep(attempt * 2)
              token = token_body["access_token"]

              smtp = {
                  "host": os.environ["KEYCLOAK_SMTP_HOST"],
                  "port": os.environ["KEYCLOAK_SMTP_PORT"],
                  "from": os.environ["KEYCLOAK_SMTP_FROM"],
                  "fromDisplayName": os.environ["KEYCLOAK_SMTP_FROM_NAME"],
                  "replyTo": os.environ["KEYCLOAK_SMTP_FROM"],
                  "replyToDisplayName": os.environ["KEYCLOAK_SMTP_FROM_NAME"],
                  "user": os.environ["KEYCLOAK_SMTP_USER"],
                  "password": os.environ["KEYCLOAK_SMTP_PASSWORD"],
                  "auth": "true",
                  "starttls": "true",
                  "ssl": "false",
              }

              status, realm_rep = request("GET", f"{base_url}/admin/realms/{realm}", token)
              if status == 404:
                  create_payload = {
                      "realm": realm,
                      "enabled": True,
                      "registrationAllowed": False,
                      "resetPasswordAllowed": True,
                      "verifyEmail": True,
                      "loginWithEmailAllowed": True,
                      "duplicateEmailsAllowed": False,
                      "smtpServer": smtp,
                  }
                  status, body = request("POST", f"{base_url}/admin/realms", token, create_payload)
                  if status not in (201, 204, 409):
                      raise SystemExit(f"Realm create failed: status={status} body={body}")
                  status, realm_rep = request("GET", f"{base_url}/admin/realms/{realm}", token)
              if status != 200 or not isinstance(realm_rep, dict):
                  raise SystemExit(f"Realm fetch failed: status={status}")

              realm_rep.update(
                  {
                      "enabled": True,
                      "registrationAllowed": False,
                      "resetPasswordAllowed": True,
                      "verifyEmail": True,
                      "loginWithEmailAllowed": True,
                      "duplicateEmailsAllowed": False,
                      "smtpServer": smtp,
                  }
              )
              status, body = request("PUT", f"{base_url}/admin/realms/{realm}", token, realm_rep)
              if status not in (200, 204):
                  raise SystemExit(f"Realm update failed: status={status} body={body}")

              def find_group(name):
                  status, groups = request(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/groups?search={urllib.parse.quote(name)}",
                      token,
                  )
                  if status != 200:
                      raise SystemExit(f"Group search failed for {name}: status={status}")
                  for group in groups or []:
                      if group.get("name") == name:
                          return group
                  return None

              def ensure_group(name):
                  group = find_group(name)
                  if group:
                      return group
                  status, body = request("POST", f"{base_url}/admin/realms/{realm}/groups", token, {"name": name})
                  if status not in (201, 204, 409):
                      raise SystemExit(f"Group create failed for {name}: status={status} body={body}")
                  group = find_group(name)
                  if not group:
                      raise SystemExit(f"Group missing after create for {name}")
                  return group

              def find_user(username):
                  encoded = urllib.parse.quote(username)
                  for query in (f"username={encoded}&exact=true", f"email={encoded}&exact=true", f"search={encoded}"):
                      status, users = request(
                          "GET",
                          f"{base_url}/admin/realms/{realm}/users?{query}",
                          token,
                      )
                      if status != 200:
                          raise SystemExit(f"User search failed for {username}: status={status}")
                      for user in users or []:
                          candidates = {
                              str(user.get("username") or "").casefold(),
                              str(user.get("email") or "").casefold(),
                          }
                          if username.casefold() in candidates:
                              return user
                  return None

              def ensure_user_group(username, group):
                  user = find_user(username)
                  if not user:
                      print(f"WARNING: Veles public replay user {username!r} not found; skipping membership")
                      return
                  user_id = user.get("id")
                  group_id = group.get("id")
                  if not user_id or not group_id:
                      raise SystemExit(f"Missing id for user/group membership: user={username}")
                  status, memberships = request(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/users/{user_id}/groups",
                      token,
                  )
                  if status != 200:
                      raise SystemExit(f"User group lookup failed for {username}: status={status}")
                  if any(item.get("id") == group_id for item in memberships or []):
                      return
                  status, body = request(
                      "PUT",
                      f"{base_url}/admin/realms/{realm}/users/{user_id}/groups/{group_id}",
                      token,
                  )
                  if status not in (200, 204):
                      raise SystemExit(f"User group update failed for {username}: status={status} body={body}")

              ensure_group("alpha")
              ensure_group("admin")
              public_replay_group = ensure_group("public-replay")
              for username in os.environ.get("VELES_PUBLIC_REPLAY_USERS", "").split(","):
                  username = username.strip()
                  if username:
                      ensure_user_group(username, public_replay_group)

              status, clients = request(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(client_id)}",
                  token,
              )
              if status != 200:
                  raise SystemExit(f"Client lookup failed: status={status}")
              client_uuid = clients[0]["id"] if clients else None
              client_payload = {
                  "clientId": client_id,
                  "enabled": True,
                  "protocol": "openid-connect",
                  "publicClient": False,
                  "standardFlowEnabled": True,
                  "implicitFlowEnabled": False,
                  "directAccessGrantsEnabled": False,
                  "serviceAccountsEnabled": False,
                  "redirectUris": [f"{veles_base_url}/*"],
                  "webOrigins": [veles_base_url],
                  "rootUrl": veles_base_url,
                  "baseUrl": "/",
                  "attributes": {
                      "pkce.code.challenge.method": "S256",
                      "post.logout.redirect.uris": f"{veles_base_url}/*",
                  },
              }
              if not client_uuid:
                  status, body = request("POST", f"{base_url}/admin/realms/{realm}/clients", token, client_payload)
                  if status not in (201, 204, 409):
                      raise SystemExit(f"Client create failed: status={status} body={body}")
                  status, clients = request(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(client_id)}",
                      token,
                  )
                  client_uuid = clients[0]["id"] if clients else None
              if not client_uuid:
                  raise SystemExit("Client veles-web not found after create")
              status, body = request(
                  "PUT",
                  f"{base_url}/admin/realms/{realm}/clients/{client_uuid}",
                  token,
                  client_payload,
              )
              if status not in (200, 204):
                  raise SystemExit(f"Client update failed: status={status} body={body}")

              mapper_payload = {
                  "name": "groups",
                  "protocol": "openid-connect",
                  "protocolMapper": "oidc-group-membership-mapper",
                  "consentRequired": False,
                  "config": {
                      "full.path": "false",
                      "id.token.claim": "true",
                      "access.token.claim": "true",
                      "userinfo.token.claim": "true",
                      "claim.name": "groups",
                      "jsonType.label": "String",
                  },
              }
              status, mappers = request(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models",
                  token,
              )
              if status != 200:
                  raise SystemExit(f"Mapper lookup failed: status={status}")
              mapper_id = next((mapper.get("id") for mapper in mappers or [] if mapper.get("name") == "groups"), None)
              if mapper_id:
                  mapper_update_payload = dict(mapper_payload)
                  mapper_update_payload["id"] = mapper_id
                  status, body = request(
                      "PUT",
                      f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models/{mapper_id}",
                      token,
                      mapper_update_payload,
                  )
              else:
                  status, body = request(
                      "POST",
                      f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/protocol-mappers/models",
                      token,
                      mapper_payload,
                  )
              if status not in (200, 201, 204):
                  raise SystemExit(f"Mapper ensure failed: status={status} body={body}")

              status, secret = request(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/clients/{client_uuid}/client-secret",
                  token,
              )
              client_secret = (secret or {}).get("value")
              if status != 200 or not client_secret:
                  raise SystemExit(f"Client secret fetch failed: status={status}")

              vault_addr = os.environ.get("VAULT_ADDR", "http://vault.vault.svc.cluster.local:8200")
              jwt = open("/var/run/secrets/kubernetes.io/serviceaccount/token", encoding="utf-8").read().strip()
              login_payload = json.dumps({"jwt": jwt, "role": os.environ.get("VAULT_ROLE", "sso-secrets")}).encode()
              req = urllib.request.Request(
                  f"{vault_addr}/v1/auth/kubernetes/login",
                  data=login_payload,
                  headers={"Content-Type": "application/json"},
                  method="POST",
              )
              with urllib.request.urlopen(req, timeout=20) as resp:
                  vault_token = json.loads(resp.read().decode())["auth"]["client_token"]

              payload = {
                  "data": {
                      "client_id": client_id,
                      "client_secret": client_secret,
                      "issuer": issuer,
                      "realm": realm,
                      "required_groups": "alpha,admin",
                  }
              }
              req = urllib.request.Request(
                  f"{vault_addr}/v1/kv/data/atlas/veles/veles-oidc",
                  data=json.dumps(payload).encode(),
                  headers={"X-Vault-Token": vault_token, "Content-Type": "application/json"},
                  method="POST",
              )
              with urllib.request.urlopen(req, timeout=20) as resp:
                  if resp.status not in (200, 204):
                      raise SystemExit(f"Vault write returned {resp.status}")

              print("Veles Keycloak realm/client ready")
              PY
