diff --git a/services/keycloak/ldap-federation-job.yaml b/services/keycloak/ldap-federation-job.yaml index f993fef..5c59fff 100644 --- a/services/keycloak/ldap-federation-job.yaml +++ b/services/keycloak/ldap-federation-job.yaml @@ -2,7 +2,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: keycloak-ldap-federation-3 + name: keycloak-ldap-federation-4 namespace: sso spec: backoffLimit: 2 @@ -60,6 +60,7 @@ spec: import os import time import urllib.parse + import urllib.error import urllib.request base_url = os.environ["KEYCLOAK_SERVER"].rstrip("/") @@ -176,11 +177,21 @@ spec: raise SystemExit(f"Unexpected components response: {status}") components = components or [] - ldap_component = None - for c in components: - if c.get("providerId") == "ldap" and c.get("name") in ("openldap", "ldap"): - ldap_component = c - break + ldap_components = [c for c in components if c.get("providerId") == "ldap" and c.get("id")] + + # Select a canonical LDAP federation provider deterministically. + # Duplicate LDAP providers can cause Keycloak admin/user queries to fail if any one of them is misconfigured. + candidates = [] + for c in ldap_components: + if c.get("name") not in ("openldap", "ldap"): + continue + cfg = c.get("config") or {} + if (cfg.get("connectionUrl") or [None])[0] == ldap_url: + candidates.append(c) + if not candidates: + candidates = [c for c in ldap_components if c.get("name") in ("openldap", "ldap")] + candidates.sort(key=lambda x: x.get("id", "")) + ldap_component = candidates[0] if candidates else None ldap_component_id = ldap_component["id"] if ldap_component else None desired = { @@ -296,4 +307,56 @@ spec: ) if status not in (201, 204): raise SystemExit(f"Unexpected group mapper create status: {status}") + + # Cleanup duplicate LDAP federation providers and their child components (mappers, etc). + # Keep only the canonical provider we updated/created above. + try: + status, fresh_components, _ = http_json( + "GET", + f"{base_url}/admin/realms/{realm}/components", + token, + ) + if status != 200: + raise Exception(f"unexpected components status {status}") + fresh_components = fresh_components or [] + + dup_provider_ids = [] + for c in fresh_components: + if c.get("providerId") != "ldap": + continue + if c.get("providerType") != "org.keycloak.storage.UserStorageProvider": + continue + cid = c.get("id") + if not cid or cid == ldap_component_id: + continue + dup_provider_ids.append(cid) + + if dup_provider_ids: + for pid in dup_provider_ids: + # Delete child components first. + for child in fresh_components: + if child.get("parentId") != pid: + continue + child_id = child.get("id") + if not child_id: + continue + try: + http_json( + "DELETE", + f"{base_url}/admin/realms/{realm}/components/{child_id}", + token, + ) + except urllib.error.HTTPError as e: + print(f"WARNING: failed to delete LDAP child component {child_id} (status={e.code})") + try: + http_json( + "DELETE", + f"{base_url}/admin/realms/{realm}/components/{pid}", + token, + ) + except urllib.error.HTTPError as e: + print(f"WARNING: failed to delete duplicate LDAP provider {pid} (status={e.code})") + print(f"Cleaned up {len(dup_provider_ids)} duplicate LDAP federation providers") + except Exception as e: + print(f"WARNING: LDAP cleanup failed (continuing): {e}") PY