# services/keycloak/bootstrap-jobs/realm-settings-job.yaml
# One-off job for sso/keycloak-realm-settings-37.
# Purpose: keycloak realm settings 37 (see container args/env in this file).
# This revision runs once to ensure the maintenance group exists for Metis access.
# Safe to delete the finished Job/pod; it should not run continuously.
apiVersion: batch/v1
kind: Job
metadata:
  name: keycloak-realm-settings-37
  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: KEYCLOAK_SMTP_HOST
              value: mail.bstein.dev
            - name: KEYCLOAK_SMTP_PORT
              value: "587"
            - name: KEYCLOAK_SMTP_FROM
              value: no-reply-sso@bstein.dev
            - name: KEYCLOAK_SMTP_FROM_NAME
              value: Atlas SSO
            - name: KEYCLOAK_SMTP_REPLY_TO
              value: no-reply-sso@bstein.dev
            - name: KEYCLOAK_SMTP_REPLY_TO_NAME
              value: Atlas SSO
          command: ["/bin/sh", "-c"]
          args:
            - |
              set -euo pipefail
              . /vault/secrets/keycloak-env.sh
              python - <<'PY'
              import json
              import os
              import time
              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"]

              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_body = None
              for attempt in range(1, 11):
                  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())
                          break
                  except urllib.error.HTTPError as exc:
                      body = exc.read().decode(errors="replace")
                      raise SystemExit(f"Token request failed: status={exc.code} body={body}")
                  except urllib.error.URLError as exc:
                      if attempt == 10:
                          raise SystemExit(f"Token request failed after retries: {exc}")
                      time.sleep(attempt * 2)
              if not token_body:
                  raise SystemExit("Token request failed without response")
              access_token = token_body["access_token"]

              # Update realm settings safely by fetching the full realm representation first.
              realm_url = f"{base_url}/admin/realms/{realm}"
              status, realm_rep = http_json("GET", realm_url, access_token)
              if status != 200 or not realm_rep:
                  raise SystemExit(f"Unable to fetch realm {realm} (status={status})")

              realm_rep["resetPasswordAllowed"] = True

              smtp = realm_rep.get("smtpServer") or {}
              smtp.update(
                  {
                      "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_REPLY_TO"],
                      "replyToDisplayName": os.environ["KEYCLOAK_SMTP_REPLY_TO_NAME"],
                      "user": os.environ["KEYCLOAK_SMTP_USER"],
                      "password": os.environ["KEYCLOAK_SMTP_PASSWORD"],
                      "auth": "true",
                      "starttls": "true",
                      "ssl": "false",
                  }
              )
              realm_rep["smtpServer"] = smtp

              status, _ = http_json("PUT", realm_url, access_token, realm_rep)
              if status not in (200, 204):
                  raise SystemExit(f"Unexpected realm update response: {status}")

              # Ensure required custom user-profile attributes exist.
              profile_url = f"{base_url}/admin/realms/{realm}/users/profile"
              status, profile = http_json("GET", profile_url, access_token)
              if status == 200 and isinstance(profile, dict):
                  attrs = profile.get("attributes")
                  if not isinstance(attrs, list):
                      attrs = []

                  required_attrs = [
                      {
                          "name": "vaultwarden_email",
                          "displayName": "Vaultwarden Email",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"email": {}, "length": {"max": 255}},
                      },
                      {
                          "name": "vaultwarden_status",
                          "displayName": "Vaultwarden Status",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 64}},
                      },
                      {
                          "name": "vaultwarden_synced_at",
                          "displayName": "Vaultwarden Last Synced",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 64}},
                      },
                      {
                          "name": "mailu_email",
                          "displayName": "Atlas Mailbox",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"email": {}, "length": {"max": 255}},
                      },
                      {
                          "name": "mailu_app_password",
                          "displayName": "Atlas Mail App Password",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 255}},
                      },
                      {
                          "name": "mailu_enabled",
                          "displayName": "Atlas Mailbox Enabled",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 16}},
                      },
                      {
                          "name": "nextcloud_mail_primary_email",
                          "displayName": "Nextcloud Mail Primary Email",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"email": {}, "length": {"max": 255}},
                      },
                      {
                          "name": "nextcloud_mail_account_count",
                          "displayName": "Nextcloud Mail Account Count",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 32}},
                      },
                      {
                          "name": "nextcloud_mail_synced_at",
                          "displayName": "Nextcloud Mail Last Synced",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 64}},
                      },
                      {
                          "name": "wger_password",
                          "displayName": "Wger Password",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 255}},
                      },
                      {
                          "name": "wger_password_updated_at",
                          "displayName": "Wger Password Updated At",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 64}},
                      },
                      {
                          "name": "firefly_password",
                          "displayName": "Firefly Password",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 255}},
                      },
                      {
                          "name": "firefly_password_updated_at",
                          "displayName": "Firefly Password Updated At",
                          "multivalued": False,
                          "annotations": {"group": "user-metadata"},
                          "permissions": {"view": ["admin"], "edit": ["admin"]},
                          "validations": {"length": {"max": 64}},
                      },
                  ]

                  def has_attr(name: str) -> bool:
                      return any(isinstance(item, dict) and item.get("name") == name for item in attrs)

                  updated = False
                  for attr in required_attrs:
                      if not has_attr(attr.get("name", "")):
                          attrs.append(attr)
                          updated = True

                  if updated:
                      profile["attributes"] = attrs
                      status, _ = http_json("PUT", profile_url, access_token, profile)
                      if status not in (200, 204):
                          raise SystemExit(f"Unexpected user-profile update response: {status}")

              def find_group(group_name: str):
                  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):
                      return None
                  for item in groups:
                      if isinstance(item, dict) and item.get("name") == group_name:
                          return item
                  return None

              def ensure_group(group_name: str):
                  group = find_group(group_name)
                  if group:
                      return group
                  status, _ = http_json(
                      "POST",
                      f"{base_url}/admin/realms/{realm}/groups",
                      access_token,
                      {"name": group_name},
                  )
                  if status not in (201, 204):
                      raise SystemExit(f"Unexpected group create response for {group_name}: {status}")
                  return find_group(group_name)

              # Ensure basic realm groups exist for provisioning.
              ensure_group("dev")
              ensure_group("admin")
              ensure_group("maintenance")
              ensure_group("demo")
              ensure_group("test")
              ensure_group("vaultwarden_grandfathered")
              planka_group = ensure_group("planka-users")

              if planka_group and planka_group.get("id"):
                  group_id = planka_group["id"]
                  status, default_groups = http_json(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/default-groups",
                      access_token,
                  )
                  default_ids = set()
                  if status == 200 and isinstance(default_groups, list):
                      for item in default_groups:
                          if isinstance(item, dict) and item.get("id"):
                              default_ids.add(item["id"])

                  if group_id not in default_ids:
                      status, _ = http_json(
                          "PUT",
                          f"{base_url}/admin/realms/{realm}/default-groups/{group_id}",
                          access_token,
                      )
                      if status not in (200, 201, 204):
                          status, _ = http_json(
                              "POST",
                              f"{base_url}/admin/realms/{realm}/default-groups/{group_id}",
                              access_token,
                          )
                          if status not in (200, 201, 204):
                              raise SystemExit(
                                  f"Unexpected default-group update response for planka-users: {status}"
                              )

                  # Ensure all existing users are in the planka-users group.
                  first = 0
                  page_size = 100
                  while True:
                      status, users = http_json(
                          "GET",
                          f"{base_url}/admin/realms/{realm}/users?first={first}&max={page_size}",
                          access_token,
                      )
                      if status != 200 or not isinstance(users, list) or not users:
                          break
                      for user in users:
                          user_id = user.get("id") if isinstance(user, dict) else None
                          if not user_id:
                              continue
                          status, groups = http_json(
                              "GET",
                              f"{base_url}/admin/realms/{realm}/users/{user_id}/groups",
                              access_token,
                          )
                          if status == 200 and isinstance(groups, list):
                              already = any(isinstance(g, dict) and g.get("id") == group_id for g in groups)
                              if already:
                                  continue
                          status, _ = http_json(
                              "PUT",
                              f"{base_url}/admin/realms/{realm}/users/{user_id}/groups/{group_id}",
                              access_token,
                          )
                          if status not in (200, 201, 204):
                              status, _ = http_json(
                                  "POST",
                                  f"{base_url}/admin/realms/{realm}/users/{user_id}/groups/{group_id}",
                                  access_token,
                              )
                              if status not in (200, 201, 204):
                                  raise SystemExit(
                                      f"Unexpected group membership update for user {user_id}: {status}"
                                  )
                      if len(users) < page_size:
                          break
                      first += page_size

              # Ensure Planka client exposes groups in userinfo for role mapping.
              status, clients = http_json(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/clients?clientId=planka",
                  access_token,
              )
              planka_client = None
              if status == 200 and isinstance(clients, list):
                  for item in clients:
                      if isinstance(item, dict) and item.get("clientId") == "planka":
                          planka_client = item
                          break

              if planka_client:
                  client_id = planka_client.get("id")
                  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 = http_json(
                      "GET",
                      f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                      access_token,
                  )
                  existing = None
                  if status == 200 and isinstance(mappers, list):
                      for item in mappers:
                          if isinstance(item, dict) and item.get("name") == mapper_payload["name"]:
                              existing = item
                              break

                  if existing and existing.get("id"):
                      mapper_payload["id"] = existing["id"]
                      status, _ = http_json(
                          "PUT",
                          f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models/{existing['id']}",
                          access_token,
                          mapper_payload,
                      )
                      if status not in (200, 204):
                          raise SystemExit(f"Unexpected protocol mapper update response: {status}")
                  else:
                      status, _ = http_json(
                          "POST",
                          f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                          access_token,
                          mapper_payload,
                      )
                      if status not in (201, 204):
                          raise SystemExit(f"Unexpected protocol mapper create response: {status}")

              # Ensure mailu_email overrides email claim for service clients.
              excluded_email_clients = {
                  "account",
                  "account-console",
                  "admin-cli",
                  "security-admin-console",
                  "realm-management",
                  "broker",
              }
              status, clients = http_json(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/clients",
                  access_token,
              )
              if status == 200 and isinstance(clients, list):
                  for client in clients:
                      if not isinstance(client, dict):
                          continue
                      if client.get("protocol") != "openid-connect":
                          continue
                      client_name = client.get("clientId") if isinstance(client.get("clientId"), str) else ""
                      if not client_name or client_name in excluded_email_clients:
                          continue
                      client_id = client.get("id")
                      if not client_id:
                          continue
                      email_mapper = {
                          "name": "mailu-email",
                          "protocol": "openid-connect",
                          "protocolMapper": "oidc-usermodel-attribute-mapper",
                          "consentRequired": False,
                          "config": {
                              "user.attribute": "mailu_email",
                              "claim.name": "email",
                              "jsonType.label": "String",
                              "id.token.claim": "true",
                              "access.token.claim": "true",
                              "userinfo.token.claim": "true",
                              "multivalued": "false",
                              "aggregate.attrs": "false",
                          },
                      }
                      status, mappers = http_json(
                          "GET",
                          f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                          access_token,
                      )
                      existing = None
                      if status == 200 and isinstance(mappers, list):
                          for item in mappers:
                              if isinstance(item, dict) and item.get("name") == email_mapper["name"]:
                                  existing = item
                                  break
                      if existing and existing.get("id"):
                          email_mapper["id"] = existing["id"]
                          status, _ = http_json(
                              "PUT",
                              f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models/{existing['id']}",
                              access_token,
                              email_mapper,
                          )
                          if status not in (200, 204):
                              raise SystemExit(f"Unexpected mailu email mapper update response: {status}")
                      else:
                          status, _ = http_json(
                              "POST",
                              f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                              access_token,
                              email_mapper,
                          )
                          if status not in (201, 204):
                              raise SystemExit(f"Unexpected mailu email mapper create response: {status}")

                      mailu_claim_mapper = {
                          "name": "mailu-email-claim",
                          "protocol": "openid-connect",
                          "protocolMapper": "oidc-usermodel-attribute-mapper",
                          "consentRequired": False,
                          "config": {
                              "user.attribute": "mailu_email",
                              "claim.name": "mailu_email",
                              "jsonType.label": "String",
                              "id.token.claim": "true",
                              "access.token.claim": "true",
                              "userinfo.token.claim": "true",
                              "multivalued": "false",
                              "aggregate.attrs": "false",
                          },
                      }
                      status, mappers = http_json(
                          "GET",
                          f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                          access_token,
                      )
                      existing_claim = None
                      if status == 200 and isinstance(mappers, list):
                          for item in mappers:
                              if isinstance(item, dict) and item.get("name") == mailu_claim_mapper["name"]:
                                  existing_claim = item
                                  break
                      if existing_claim and existing_claim.get("id"):
                          mailu_claim_mapper["id"] = existing_claim["id"]
                          status, _ = http_json(
                              "PUT",
                              f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models/{existing_claim['id']}",
                              access_token,
                              mailu_claim_mapper,
                          )
                          if status not in (200, 204):
                              raise SystemExit(f"Unexpected mailu email claim mapper update response: {status}")
                      else:
                          status, _ = http_json(
                              "POST",
                              f"{base_url}/admin/realms/{realm}/clients/{client_id}/protocol-mappers/models",
                              access_token,
                              mailu_claim_mapper,
                          )
                          if status not in (201, 204):
                              raise SystemExit(f"Unexpected mailu email claim mapper create response: {status}")

              # Ensure MFA is on by default for newly-created users.
              status, required_actions = http_json(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/authentication/required-actions",
                  access_token,
              )
              if status == 200 and isinstance(required_actions, list):
                  for action in required_actions:
                      if not isinstance(action, dict):
                          continue
                      if action.get("alias") != "CONFIGURE_TOTP":
                          continue
                      if action.get("enabled") is True and action.get("defaultAction") is True:
                          break
                      action["enabled"] = True
                      action["defaultAction"] = True
                      status, _ = http_json(
                          "PUT",
                          f"{base_url}/admin/realms/{realm}/authentication/required-actions/CONFIGURE_TOTP",
                          access_token,
                          action,
                      )
                      if status not in (200, 204):
                          raise SystemExit(
                              f"Unexpected required-action update response for CONFIGURE_TOTP: {status}"
                          )

              # Disable Identity Provider Redirector in the browser flow for this realm.
              status, executions = http_json(
                  "GET",
                  f"{base_url}/admin/realms/{realm}/authentication/flows/browser/executions",
                  access_token,
              )
              if status == 200 and executions:
                  for ex in executions:
                      if ex.get("providerId") != "identity-provider-redirector":
                          continue
                      if ex.get("requirement") == "DISABLED":
                          continue
                      ex["requirement"] = "DISABLED"
                      status, _ = http_json(
                          "PUT",
                          f"{base_url}/admin/realms/{realm}/authentication/flows/browser/executions",
                          access_token,
                          ex,
                      )
                      if status not in (200, 204):
                          raise SystemExit(
                              f"Unexpected execution update response for identity-provider-redirector: {status}"
                          )
              PY
          volumeMounts:
