{{- /*
Catalyst bp-keycloak — Sovereign realm import ConfigMap + catalyst-api service-account
credentials Secret (issue #604 / #608, Phase-8b handover).

This single template owns BOTH resources so they share the same generated
service-account client secret. Splitting them into two files would re-run
randAlphaNum independently, producing two different secrets and a permanent
authentication mismatch between Keycloak (realm import) and catalyst-api
(env vars). One file → one Helm template scope → one shared variable.

What this file emits
────────────────────
1. ConfigMap  {{ "{{ .Release.Name }}" }}-sovereign-realm-config (keycloak ns)
   - Holds the realm JSON imported by the upstream bitnami/keycloak chart's
     keycloak-config-cli post-deploy Job (existingConfigmap reference in
     values.yaml keycloak.keycloakConfigCli.existingConfigmap).
   - Defines the `sovereign` realm: catalyst-ui (PKCE), kubectl (public),
     catalyst-api-server (confidential service-account with realm-management
     impersonate + manage-users + view-users + query-users + manage-realm +
     view-realm + view-clients role mappings — manage-realm/view-realm let
     the catalyst-api EPIC-3 T2 tier-role bootstrap goroutine create the
     catalyst-{viewer,developer,operator,admin,owner} realm-roles + composite
     chain at startup; view-clients is needed to inspect the realm during
     reconciliation. See products/catalyst/bootstrap/api/internal/keycloak/
     realm_bootstrap.go for the bootstrap plan).

2. Secret    catalyst-kc-sa-credentials (keycloak ns)
   - Mirrored cross-namespace into catalyst-system by emberstack/reflector
     (bootstrap-kit slot 05a). The mirror's keys (addr, realm, client-id,
     client-secret) are consumed by catalyst-api as env vars
     CATALYST_KC_ADDR / CATALYST_KC_REALM / CATALYST_KC_SA_CLIENT_ID /
     CATALYST_KC_SA_CLIENT_SECRET — see products/catalyst/chart/templates/
     api-deployment.yaml secretKeyRef section. Without this Secret the
     /auth/handover endpoint returns "server misconfiguration: keycloak
     not configured" (auth_handover.go:169).

Secret-value resolution order (first match wins)
────────────────────────────────────────────────
  1. operator-supplied .Values.catalystApiServerClientSecret (cluster overlay)
  2. existing Secret in keycloak ns (helm `lookup` → idempotent on upgrade)
  3. fresh `randAlphaNum 32` (zero-touch on first install)

Pattern mirrors platform/powerdns/chart/templates/api-credentials-secret.yaml
(canonical seam for self-bootstrapping per-Sovereign credentials, ADR-0001
§11.3 anti-duplication). Both resources render in the same template pass so
the shared `$saSecret` variable carries through.

Why this is the canonical seam vs. a post-install Job
─────────────────────────────────────────────────────
The realm import already creates the catalyst-api-server client with role
mappings (clientScopeMappings.realm-management). The realm import takes a
literal client secret in the JSON — keycloak-config-cli persists it. So the
secret value is determined at chart-render time, not by a follow-up Admin-API
script. A Job-based approach would have to: (a) wait for Keycloak Ready, (b)
mint an admin token, (c) PUT the client secret, (d) write the K8s Secret —
that's four failure modes the lookup-or-generate pattern eliminates.

Canonical seam: platform/keycloak/chart/templates/configmap-sovereign-realm.yaml

Tenant-mode mutual exclusion (issue #915, C1)
─────────────────────────────────────────────
When `realmConfig.tenant.enabled=true` the chart is being installed for an
SME tenant (per-tenant Keycloak with a tenant realm + per-app OIDC clients
for wordpress / openclaw / stalwart). In that mode the tenant realm
ConfigMap (templates/configmap-tenant-realm.yaml) emits a ConfigMap with
the SAME name expected by `keycloakConfigCli.existingConfigmap`
(`{{"{{ .Release.Name }}"}}-sovereign-realm-config`) so the upstream
keycloak-config-cli Job imports the tenant realm instead. To avoid two
ConfigMaps colliding on the same name, this Sovereign template skips when
tenant mode is on. The orchestrator (A3 / PR #911) currently sets BOTH
`sovereignRealm.enabled=true` and `realmConfig.tenant.enabled=true` in the
tenant emit; tenant takes precedence here.
*/}}
{{- if and .Values.sovereignRealm.enabled (not (and .Values.realmConfig (and .Values.realmConfig.tenant .Values.realmConfig.tenant.enabled))) }}
{{- /* ── Resolve realm name (per-Sovereign tenant short-name) ─────────── */}}
{{- $realmName := default "sovereign" .Values.sovereignRealm.name -}}
{{- /* ── Resolve catalyst-api-server client secret ─────────────────────── */}}
{{- $saSecretName := "catalyst-kc-sa-credentials" -}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace $saSecretName -}}
{{- $saSecret := "" -}}
{{- if $existing -}}
  {{- /* Idempotent: reuse on upgrade so the realm-import client secret
         stays stable across Helm reconciles. */ -}}
  {{- $saSecret = index $existing.data "client-secret" | b64dec -}}
{{- end -}}
{{- if .Values.catalystApiServerClientSecret -}}
  {{- /* Operator override wins — useful for replicating an existing
         per-Sovereign secret from a sealed-secret or external store. */ -}}
  {{- $saSecret = .Values.catalystApiServerClientSecret -}}
{{- end -}}
{{- if not $saSecret -}}
  {{- /* Genuine first install — generate 32-char random per
         INVIOLABLE-PRINCIPLES #10. */ -}}
  {{- $saSecret = randAlphaNum 32 -}}
{{- end -}}
{{- /* Keycloak base URL — issue #781.
       This URL is consumed EXCLUSIVELY by the in-cluster catalyst-api Pod
       (via the catalyst-kc-sa-credentials Secret reflected into
       catalyst-system) for intra-cluster OAuth client_credentials calls
       (token mint, EnsureUser, etc.). It is NEVER exposed to browsers —
       browsers reach Keycloak through the Cilium Gateway HTTPRoute at
       https://<gateway.host>.

       Why we use the in-cluster Service URL, not https://auth.<sov-fqdn>:
       on a Sovereign, the cluster's CoreDNS resolves *.<sov-fqdn> via the
       upstream resolvers — it does NOT forward to the in-cluster
       PowerDNS that holds those records. Result: Pod-side lookups of
       auth.<sov-fqdn> return NXDOMAIN even though public DNS is healthy.
       Caught live on otech94 2026-05-04: handover URL returned
       `{"error":"keycloak error: ensure user"}` from a
       `dial tcp: lookup auth.otech94.omani.works ... no such host`
       inside the catalyst-api Pod.

       The fix: write the Service URL into the Secret. CoreDNS resolves
       <release>.<namespace>.svc.cluster.local natively (kube-dns plugin),
       so the lookup never leaves the cluster. The HTTPRoute hostname
       (.Values.gateway.host) is unaffected — it remains the operator-
       facing URL for browsers. Inviolable Principle #4 (never hardcode)
       still holds: the URL is computed from .Release.Name + .Release.Namespace,
       both runtime-configurable via the bp-keycloak HelmRelease. */ -}}
{{- $kcAddr := printf "http://%s.%s.svc.cluster.local" .Release.Name .Release.Namespace -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-sovereign-realm-config
  namespace: {{ .Release.Namespace }}
  labels:
    catalyst.openova.io/blueprint: bp-keycloak
    catalyst.openova.io/component: keycloak-config-cli
    {{- with .Values.sovereignFQDN }}
    catalyst.openova.io/sovereign-fqdn: {{ . | quote }}
    {{- end }}
data:
  sovereign-realm.json: |
    {
      "realm": {{ $realmName | quote }},
      "enabled": true,
      "displayName": {{ default "Sovereign" .Values.sovereignRealm.displayName | quote }},
      "sslRequired": "external",
      "registrationAllowed": false,
      "loginWithEmailAllowed": true,
      "duplicateEmailsAllowed": false,
      "resetPasswordAllowed": true,
      "editUsernameAllowed": false,
      "bruteForceProtected": true,
      "accessTokenLifespan": 900,
      "ssoSessionIdleTimeout": 1800,
      "ssoSessionMaxLifespan": 28800,
      "attributes": {
        "token-exchange": "true"
      },
      {{- if and .Values.smtp .Values.smtp.host }}
      "smtpServer": {
        "host": {{ .Values.smtp.host | quote }},
        "port": {{ .Values.smtp.port | default "587" | quote }},
        "from": {{ .Values.smtp.from | default "noreply@openova.io" | quote }},
        "fromDisplayName": "Sovereign Cloud",
        "ssl": {{ .Values.smtp.ssl | default "false" | quote }},
        "starttls": {{ .Values.smtp.starttls | default "true" | quote }},
        "auth": {{ .Values.smtp.auth | default "true" | quote }}
        {{- if and .Values.smtp.user .Values.smtp.user }},
        "user": {{ .Values.smtp.user | quote }}
        {{- end }}
      },
      {{- end }}
      "defaultGroups": [
        "/sovereign-viewers"
      ],
      "groups": [
        { "name": "sovereign-admins" },
        { "name": "sovereign-ops" },
        { "name": "sovereign-viewers" }
      ],
      "requiredActions": [
        {
          "alias": "UPDATE_PASSWORD",
          "name": "Update Password",
          "providerId": "UPDATE_PASSWORD",
          "enabled": true,
          "defaultAction": true,
          "priority": 30,
          "config": {}
        },
        {
          "alias": "webauthn-register",
          "name": "Webauthn Register",
          "providerId": "webauthn-register",
          "enabled": true,
          "defaultAction": false,
          "priority": 70,
          "config": {}
        },
        {
          "alias": "webauthn-register-passwordless",
          "name": "Webauthn Register Passwordless",
          "providerId": "webauthn-register-passwordless",
          "enabled": true,
          "defaultAction": false,
          "priority": 80,
          "config": {}
        }
      ],
      "clientScopes": [
        {
          "name": "groups",
          "protocol": "openid-connect",
          "attributes": {
            "include.in.token.scope": "true",
            "display.on.consent.screen": "true"
          },
          "protocolMappers": [
            {
              "name": "groups",
              "protocol": "openid-connect",
              "protocolMapper": "oidc-group-membership-mapper",
              "consentRequired": false,
              "config": {
                "full.path": "false",
                "id.token.claim": "true",
                "access.token.claim": "true",
                "claim.name": "groups",
                "userinfo.token.claim": "true"
              }
            }
          ]
        },
        {
          "name": "required-actions",
          "protocol": "openid-connect",
          "attributes": {
            "include.in.token.scope": "true",
            "display.on.consent.screen": "false"
          },
          "protocolMappers": [
            {
              "name": "required-actions",
              "protocol": "openid-connect",
              "protocolMapper": "oidc-usermodel-attribute-mapper",
              "consentRequired": false,
              "config": {
                "user.attribute": "requiredActions",
                "id.token.claim": "true",
                "access.token.claim": "false",
                "claim.name": "requiredActions",
                "userinfo.token.claim": "true",
                "multivalued": "true",
                "jsonType.label": "String"
              }
            }
          ]
        }
      ],
      "clients": [
        {
          "clientId": "kubectl",
          "name": "kubectl (per-Sovereign K8s api-server OIDC client)",
          "description": "Public OIDC client for kubectl OIDC login against k3s api-server (issue #326). Left intact from bp-keycloak 1.2.x.",
          "enabled": true,
          "publicClient": true,
          "standardFlowEnabled": true,
          "directAccessGrantsEnabled": true,
          "implicitFlowEnabled": false,
          "serviceAccountsEnabled": false,
          "redirectUris": [
            "http://localhost:8000",
            "http://localhost:8000/*",
            "urn:ietf:wg:oauth:2.0:oob"
          ],
          "webOrigins": [
            "+"
          ],
          "defaultClientScopes": [
            "web-origins",
            "acr",
            "profile",
            "roles",
            "email",
            "groups"
          ],
          "optionalClientScopes": [
            "address",
            "phone",
            "offline_access",
            "microprofile-jwt"
          ],
          "attributes": {
            "pkce.code.challenge.method": "S256",
            "access.token.lifespan": "900"
          }
        },
        {
          "clientId": "catalyst-ui",
          "name": "Catalyst UI (Sovereign console)",
          "description": "Public PKCE client for the catalyst-ui SPA at console.<sovereignFQDN>. ID token includes groups + requiredActions claims for first-login UX (issue #604).",
          "enabled": true,
          "publicClient": true,
          "standardFlowEnabled": true,
          "directAccessGrantsEnabled": false,
          "implicitFlowEnabled": false,
          "serviceAccountsEnabled": false,
          "redirectUris": [
            "https://console.{{ .Values.sovereignFQDN }}/*"
          ],
          "webOrigins": [
            "https://console.{{ .Values.sovereignFQDN }}"
          ],
          "defaultClientScopes": [
            "web-origins",
            "acr",
            "profile",
            "roles",
            "email",
            "groups"
          ],
          "optionalClientScopes": [
            "address",
            "phone",
            "offline_access",
            "microprofile-jwt",
            "required-actions"
          ],
          "attributes": {
            "pkce.code.challenge.method": "S256",
            "access.token.lifespan": "900",
            "post.logout.redirect.uris": "https://console.{{ .Values.sovereignFQDN }}/*"
          }
        },
        {
          "clientId": "catalyst-api-server",
          "name": "Catalyst API Server (service account)",
          "description": "Service-account client for catalyst-api: token-exchange (impersonate, manage-users) + tier-role bootstrap (manage-realm, view-realm, view-clients).",
          "enabled": true,
          "publicClient": false,
          "standardFlowEnabled": false,
          "directAccessGrantsEnabled": false,
          "implicitFlowEnabled": false,
          "serviceAccountsEnabled": true,
          "authorizationServicesEnabled": false,
          "secret": {{ $saSecret | quote }},
          "redirectUris": [],
          "webOrigins": [],
          "defaultClientScopes": [
            "web-origins",
            "acr",
            "profile",
            "roles",
            "email"
          ],
          "optionalClientScopes": [
            "address",
            "phone",
            "offline_access",
            "microprofile-jwt"
          ],
          "attributes": {
            "access.token.lifespan": "300"
          }
        }
      ],
      "clientScopeMappings": {
        "realm-management": [
          {
            "client": "catalyst-api-server",
            "roles": [
              "impersonation",
              "manage-users",
              "view-users",
              "query-users",
              "manage-realm",
              "view-realm",
              "view-clients"
            ]
          }
        ]
      },
      "users": [
        {
          "username": "service-account-catalyst-api-server",
          "enabled": true,
          "serviceAccountClientId": "catalyst-api-server",
          "clientRoles": {
            "realm-management": [
              "impersonation",
              "manage-users",
              "view-users",
              "query-users",
              "manage-realm",
              "view-realm",
              "view-clients"
            ]
          }
        }
      ]
    }
---
{{- /* ── catalyst-kc-sa-credentials Secret ─────────────────────────────────
       Mirrored to catalyst-system by reflector. Keys MUST match the
       secretKeyRef.key values in products/catalyst/chart/templates/
       api-deployment.yaml: addr / realm / client-id / client-secret. */}}
apiVersion: v1
kind: Secret
metadata:
  name: {{ $saSecretName }}
  namespace: {{ .Release.Namespace }}
  labels:
    catalyst.openova.io/blueprint: bp-keycloak
    catalyst.openova.io/component: catalyst-api-sa-credentials
    {{- with .Values.sovereignFQDN }}
    catalyst.openova.io/sovereign-fqdn: {{ . | quote }}
    {{- end }}
  annotations:
    catalyst.openova.io/comment: |
      catalyst-api-server OIDC client credentials. The client itself is
      created in the sovereign realm by the keycloak-config-cli realm
      import (see ConfigMap above). This Secret carries the same
      client-secret value used in the realm JSON so catalyst-api can
      authenticate to Keycloak via OAuth2 client_credentials grant for
      /auth/handover and impersonation calls.

      First-install: Helm `lookup` returns nothing -> randAlphaNum 32 ->
      both this Secret and the realm JSON share the value (single
      template scope).
      Upgrade: Helm `lookup` returns existing Secret -> re-emits same
      bytes -> realm JSON re-emits same bytes -> no rotation, no drift.
      Operator override: set .Values.catalystApiServerClientSecret in
      cluster overlay to pin a known value (e.g. for sealed-secret
      replication scenarios).
    # Reflector cross-namespace mirror — catalyst-api lives in
    # catalyst-system (bootstrap-kit slot 13). Auto-mirror so the
    # destination Secret materialises as soon as catalyst-system
    # namespace exists, even if bp-keycloak installs first.
    reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
    reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "catalyst-system"
    reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
    reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "catalyst-system"
type: Opaque
stringData:
  addr: {{ $kcAddr | quote }}
  realm: {{ $realmName | quote }}
  client-id: "catalyst-api-server"
  client-secret: {{ $saSecret | quote }}
{{- end }}
