# services/keycloak/user-overrides-job.yaml apiVersion: batch/v1 kind: Job metadata: name: keycloak-user-overrides-6 namespace: sso spec: 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 "relay-username" }}" export KEYCLOAK_SMTP_PASSWORD="{{ index .Data.data "relay-password" }}" {{ 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") if isinstance(existing, list) and existing and existing[0] == override_mailu_email: raise SystemExit(0) if isinstance(existing, str) and existing == override_mailu_email: raise SystemExit(0) 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 group for Vault access. status, groups = http_json( "GET", f"{base_url}/admin/realms/{realm}/groups?search=admin", 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") == "admin": group_id = item.get("id") or "" break if not group_id: raise SystemExit("admin 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 not already: 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: {status}") PY volumeMounts: