diff --git a/services/keycloak/kustomization.yaml b/services/keycloak/kustomization.yaml index eb554ab..80c504c 100644 --- a/services/keycloak/kustomization.yaml +++ b/services/keycloak/kustomization.yaml @@ -8,6 +8,7 @@ resources: - deployment.yaml - realm-settings-job.yaml - portal-e2e-client-job.yaml + - portal-e2e-target-client-job.yaml - ldap-federation-job.yaml - user-overrides-job.yaml - service.yaml diff --git a/services/keycloak/portal-e2e-target-client-job.yaml b/services/keycloak/portal-e2e-target-client-job.yaml new file mode 100644 index 0000000..45b3980 --- /dev/null +++ b/services/keycloak/portal-e2e-target-client-job.yaml @@ -0,0 +1,138 @@ +# services/keycloak/portal-e2e-target-client-job.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: keycloak-portal-e2e-target-1 + namespace: sso +spec: + backoffLimit: 0 + template: + spec: + restartPolicy: Never + 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_ADMIN_USER + valueFrom: + secretKeyRef: + name: keycloak-admin + key: username + - name: KEYCLOAK_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: keycloak-admin + key: password + - name: TARGET_CLIENT_ID + value: bstein-dev-home + command: ["/bin/sh", "-c"] + args: + - | + set -euo pipefail + 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"] + target_client_id = os.environ["TARGET_CLIENT_ID"] + + 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")} + + def get_admin_token() -> str: + token_data = urllib.parse.urlencode( + { + "grant_type": "password", + "client_id": "admin-cli", + "username": admin_user, + "password": admin_password, + } + ).encode() + 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(req, timeout=15) as resp: + body = json.loads(resp.read().decode()) + except urllib.error.HTTPError as exc: + raw = exc.read().decode(errors="replace") + raise SystemExit(f"Token request failed: status={exc.code} body={raw}") + return body["access_token"] + + token = get_admin_token() + + status, clients = http_json( + "GET", + f"{base_url}/admin/realms/{realm}/clients?clientId={urllib.parse.quote(target_client_id)}", + token, + ) + if status != 200 or not isinstance(clients, list) or not clients: + raise SystemExit(f"Unable to find target client {target_client_id!r} (status={status})") + + client_uuid = None + for item in clients: + if isinstance(item, dict) and item.get("clientId") == target_client_id: + client_uuid = item.get("id") + break + if not client_uuid: + raise SystemExit(f"Target client {target_client_id!r} has no id") + + status, client_rep = http_json( + "GET", + f"{base_url}/admin/realms/{realm}/clients/{client_uuid}", + token, + ) + if status != 200 or not isinstance(client_rep, dict): + raise SystemExit(f"Unable to fetch client representation (status={status})") + + attrs = client_rep.get("attributes") or {} + updated = False + if attrs.get("oauth2.token.exchange.grant.enabled") != "true": + attrs["oauth2.token.exchange.grant.enabled"] = "true" + updated = True + client_rep["attributes"] = attrs + + if updated: + status, resp = http_json( + "PUT", + f"{base_url}/admin/realms/{realm}/clients/{client_uuid}", + token, + client_rep, + ) + if status not in (200, 204): + raise SystemExit(f"Client update failed (status={status}) resp={resp}") + + print(f"OK: ensured token exchange enabled on client {target_client_id}") + PY