# services/keycloak/bootstrap-jobs/cassandra-realm-ensure-job.yaml # One-off job for sso/cassandra-realm-ensure-4. # Purpose: create the Cassandra realm, groups, OIDC client, SMTP settings, and Vault client secret. # Keep suspended until Cassandra Vault paths/policies have reconciled, then unsuspend once. apiVersion: batch/v1 kind: Job metadata: name: cassandra-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: cassandra - name: KEYCLOAK_CLIENT_ID value: cassandra-web - name: KEYCLOAK_PUBLIC_ISSUER value: https://sso.bstein.dev/realms/cassandra - name: CASSANDRA_BASE_URL value: https://cassandra.bstein.dev - name: CASSANDRA_TESTER_GROUP value: cassandra-tester - name: KEYCLOAK_SMTP_HOST value: mail.bstein.dev - name: KEYCLOAK_SMTP_PORT value: "587" - name: KEYCLOAK_SMTP_FROM value: no-reply-cassandra@bstein.dev - name: KEYCLOAK_SMTP_FROM_NAME value: Cassandra - name: CASSANDRA_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"] app_base_url = os.environ["CASSANDRA_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: Cassandra 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}") alpha_group = ensure_group("alpha") ensure_group("admin") tester_group = ensure_group(os.environ["CASSANDRA_TESTER_GROUP"]) public_replay_group = ensure_group("public-replay") pilot_groups = { "pilot-skill-proficient": ensure_group("pilot-skill-proficient"), "pilot-skill-adept": ensure_group("pilot-skill-adept"), "pilot-skill-veteran": ensure_group("pilot-skill-veteran"), "pilot-skill-expert": ensure_group("pilot-skill-expert"), "pilot-skill-master": ensure_group("pilot-skill-master"), "all-pilot-skill-level": ensure_group("all-pilot-skill-level"), "codex-cli-fallback": ensure_group("codex-cli-fallback"), } for username in os.environ.get("CASSANDRA_PUBLIC_REPLAY_USERS", "").split(","): username = username.strip() if username: ensure_user_group(username, public_replay_group) ensure_user_group("veles-dev", tester_group) ensure_user_group("veles-dev", pilot_groups["all-pilot-skill-level"]) ensure_user_group("veles-dev", pilot_groups["codex-cli-fallback"]) for username in ("daniel-test", "viktor-test"): ensure_user_group(username, alpha_group) ensure_user_group(username, tester_group) ensure_user_group(username, pilot_groups["pilot-skill-proficient"]) ensure_user_group(username, pilot_groups["pilot-skill-adept"]) ensure_user_group(username, pilot_groups["pilot-skill-veteran"]) ensure_user_group(username, pilot_groups["codex-cli-fallback"]) 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"{app_base_url}/*"], "webOrigins": [app_base_url], "rootUrl": app_base_url, "baseUrl": "/", "attributes": { "pkce.code.challenge.method": "S256", "post.logout.redirect.uris": f"{app_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 cassandra-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/cassandra/cassandra-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("Cassandra Keycloak realm/client ready") PY