#!/bin/bash
# keycloak-configure.sh — wire Keycloak to FreeIPA via LDAP user federation
#
# Run this AFTER both FreeIPA and Keycloak are fully up.
# Reads settings from environment variables or a .env file in the same directory.
#
# Required env vars:
#   IPA_SERVER           FreeIPA server FQDN
#   IPA_DOMAIN           FreeIPA domain
#   IPA_DM_PASSWORD      Directory Manager password (used as LDAP bind credential
#                        unless IPA_BIND_DN / IPA_BIND_PASSWORD override it)
#   KC_ADMIN_PASSWORD    Keycloak admin password
#
# Optional env vars (defaults shown):
#   KC_URL               http://localhost:8080
#   KC_ADMIN             admin
#   KC_REALM             freeipa          (realm to create)
#   KC_REALM_DISPLAY     <IPA_DOMAIN>
#   IPA_REALM            <IPA_DOMAIN uppercased>
#   IPA_BIND_DN          cn=Directory Manager
#   IPA_BIND_PASSWORD    <IPA_DM_PASSWORD>
#   IPA_USE_LDAPS        false
#   IPA_LDAP_PORT        389 (or 636 if LDAPS)
#   SYNC_FULL_PERIOD     604800   (1 week)
#   SYNC_CHANGED_PERIOD  86400    (1 day)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
[[ -f "$SCRIPT_DIR/.env" ]] && set -a && source "$SCRIPT_DIR/.env" && set +a

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
log()   { echo -e "${GREEN}[+]${NC} $*"; }
warn()  { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[✗]${NC} $*" >&2; }
info()  { echo -e "${CYAN}[i]${NC} $*"; }

: "${IPA_SERVER:?IPA_SERVER is required}"
: "${IPA_DOMAIN:?IPA_DOMAIN is required}"
: "${IPA_DM_PASSWORD:?IPA_DM_PASSWORD is required}"
: "${KC_ADMIN_PASSWORD:?KC_ADMIN_PASSWORD is required}"

KC_URL="${KC_URL:-http://localhost:8080}"
KC_ADMIN="${KC_ADMIN:-admin}"
KC_REALM="${KC_REALM:-freeipa}"
KC_REALM_DISPLAY="${KC_REALM_DISPLAY:-$IPA_DOMAIN}"
IPA_REALM="${IPA_REALM:-${IPA_DOMAIN^^}}"
IPA_BIND_DN="${IPA_BIND_DN:-cn=Directory Manager}"
IPA_BIND_PASSWORD="${IPA_BIND_PASSWORD:-$IPA_DM_PASSWORD}"
IPA_USE_LDAPS="${IPA_USE_LDAPS:-false}"
IPA_LDAP_SCHEME="ldap"
IPA_LDAP_PORT=389
[[ "$IPA_USE_LDAPS" == "true" ]] && IPA_LDAP_SCHEME="ldaps" && IPA_LDAP_PORT=636
IPA_LDAP_URL="${IPA_LDAP_URL:-${IPA_LDAP_SCHEME}://${IPA_SERVER}:${IPA_LDAP_PORT}}"
IPA_BASEDN="dc=${IPA_DOMAIN/./,dc=}"
SYNC_FULL_PERIOD="${SYNC_FULL_PERIOD:-604800}"
SYNC_CHANGED_PERIOD="${SYNC_CHANGED_PERIOD:-86400}"

# ─── Helpers ──────────────────────────────────────────────────────────────────
kc_token() {
    curl -sf -X POST \
        "$KC_URL/realms/master/protocol/openid-connect/token" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "client_id=admin-cli&grant_type=password" \
        -d "username=$KC_ADMIN" \
        --data-urlencode "password=$KC_ADMIN_PASSWORD" \
        | jq -r '.access_token'
}

kc_get()    { curl -sf -H "Authorization: Bearer $TOKEN" "$KC_URL$1"; }
kc_post()   { curl -sf -X POST   -H "Authorization: Bearer $TOKEN" \
                   -H "Content-Type: application/json" -d "$2" "$KC_URL$1"; }
kc_put()    { curl -sf -X PUT    -H "Authorization: Bearer $TOKEN" \
                   -H "Content-Type: application/json" -d "$2" "$KC_URL$1"; }

kc_status() { curl -sf -o /dev/null -w "%{http_code}" \
                   -H "Authorization: Bearer $TOKEN" "$KC_URL$1"; }

# ─── Wait for Keycloak ────────────────────────────────────────────────────────
info "Waiting for Keycloak at $KC_URL..."
for i in $(seq 1 60); do
    curl -sf "$KC_URL/health/ready" &>/dev/null && break
    [[ $i -eq 60 ]] && { error "Keycloak not ready after 120s."; exit 1; }
    sleep 2
done
log "Keycloak is ready."

# ─── Authenticate ─────────────────────────────────────────────────────────────
TOKEN=$(kc_token)
[[ -z "$TOKEN" || "$TOKEN" == "null" ]] && { error "Failed to obtain Keycloak token."; exit 1; }
log "Admin token obtained."

# ─── Create realm ─────────────────────────────────────────────────────────────
REALM_STATUS=$(kc_status "/admin/realms/$KC_REALM")
if [[ "$REALM_STATUS" == "200" ]]; then
    warn "Realm '$KC_REALM' already exists — updating."
    kc_put "/admin/realms/$KC_REALM" \
        "{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true,
          \"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null
else
    kc_post "/admin/realms" \
        "{\"realm\":\"$KC_REALM\",\"displayName\":\"$KC_REALM_DISPLAY\",\"enabled\":true,
          \"ssoSessionMaxLifespan\":36000,\"accessTokenLifespan\":300}" >/dev/null
    log "Realm '$KC_REALM' created."
fi

TOKEN=$(kc_token)

# ─── LDAP user federation ─────────────────────────────────────────────────────
log "Configuring FreeIPA LDAP user federation..."

LDAP_COMPONENT=$(cat <<JSON
{
  "name": "freeipa-ldap",
  "providerId": "ldap",
  "providerType": "org.keycloak.storage.UserStorageProvider",
  "config": {
    "enabled":                         ["true"],
    "priority":                        ["1"],
    "editMode":                        ["READ_ONLY"],
    "syncRegistrations":               ["false"],
    "vendor":                          ["rhds"],
    "usernameLDAPAttribute":           ["uid"],
    "rdnLDAPAttribute":                ["uid"],
    "uuidLDAPAttribute":               ["ipaUniqueID"],
    "userObjectClasses":               ["inetOrgPerson, organizationalPerson"],
    "connectionUrl":                   ["$IPA_LDAP_URL"],
    "usersDn":                         ["cn=users,cn=accounts,$IPA_BASEDN"],
    "authType":                        ["simple"],
    "bindDn":                          ["$IPA_BIND_DN"],
    "bindCredential":                  ["$IPA_BIND_PASSWORD"],
    "searchScope":                     ["1"],
    "validatePasswordPolicy":          ["false"],
    "trustEmail":                      ["false"],
    "useTruststoreSpi":                ["ldapsOnly"],
    "connectionPooling":               ["true"],
    "pagination":                      ["true"],
    "batchSizeForSync":                ["1000"],
    "fullSyncPeriod":                  ["$SYNC_FULL_PERIOD"],
    "changedSyncPeriod":               ["$SYNC_CHANGED_PERIOD"],
    "importEnabled":                   ["true"],
    "cachePolicy":                     ["DEFAULT"],
    "kerberosRealm":                   ["$IPA_REALM"],
    "serverPrincipal":                 ["HTTP/$IPA_SERVER@$IPA_REALM"],
    "useKerberosForPasswordAuthentication": ["false"],
    "allowKerberosAuthentication":     ["false"],
    "debug":                           ["false"]
  }
}
JSON
)

EXISTING_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \
    | jq -r '.[0].id // empty')

if [[ -n "$EXISTING_ID" ]]; then
    warn "LDAP provider already exists (id=$EXISTING_ID) — updating."
    kc_put "/admin/realms/$KC_REALM/components/$EXISTING_ID" "$LDAP_COMPONENT" >/dev/null
    LDAP_ID="$EXISTING_ID"
else
    LDAP_ID=$(kc_post "/admin/realms/$KC_REALM/components" "$LDAP_COMPONENT" \
        | jq -r '.id // empty')
    # Keycloak returns 201 with Location header, not a body with id — extract from header or re-query
    if [[ -z "$LDAP_ID" ]]; then
        LDAP_ID=$(kc_get "/admin/realms/$KC_REALM/components?type=org.keycloak.storage.UserStorageProvider&name=freeipa-ldap" \
            | jq -r '.[0].id')
    fi
    log "LDAP provider created (id=$LDAP_ID)."
fi

# ─── Attribute mappers ────────────────────────────────────────────────────────
log "Adding LDAP attribute mappers..."

add_mapper() {
    local name="$1" type="$2" ldap_attr="$3" user_attr="$4"
    local payload
    payload=$(cat <<JSON
{
  "name": "$name",
  "providerId": "$type",
  "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
  "parentId": "$LDAP_ID",
  "config": {
    "ldap.attribute":           ["$ldap_attr"],
    "user.model.attribute":     ["$user_attr"],
    "read.only":                ["true"],
    "always.read.value.from.ldap": ["false"],
    "is.mandatory.in.ldap":     ["false"]
  }
}
JSON
)
    local exists
    exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=$name" \
        | jq -r '.[0].id // empty')
    if [[ -z "$exists" ]]; then
        kc_post "/admin/realms/$KC_REALM/components" "$payload" >/dev/null
        log "  mapper: $name"
    else
        warn "  mapper '$name' already exists — skipping."
    fi
}

add_mapper "email"      "user-attribute-ldap-mapper" "mail"       "email"
add_mapper "first-name" "user-attribute-ldap-mapper" "givenName"  "firstName"
add_mapper "last-name"  "user-attribute-ldap-mapper" "sn"         "lastName"
add_mapper "uid-number" "user-attribute-ldap-mapper" "uidNumber"  "uidNumber"

# Group mapper (maps IPA groups to Keycloak groups)
GROUP_MAPPER=$(cat <<JSON
{
  "name": "freeipa-groups",
  "providerId": "group-ldap-mapper",
  "providerType": "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
  "parentId": "$LDAP_ID",
  "config": {
    "groups.dn":                    ["cn=groups,cn=accounts,$IPA_BASEDN"],
    "group.name.ldap.attribute":    ["cn"],
    "group.object.classes":         ["groupOfNames"],
    "preserve.group.inheritance":   ["false"],
    "membership.ldap.attribute":    ["member"],
    "membership.attribute.type":    ["DN"],
    "mode":                         ["READ_ONLY"],
    "user.roles.retrieve.strategy": ["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"],
    "mapped.group.attributes":      [""],
    "drop.non.existing.groups.during.sync": ["false"]
  }
}
JSON
)
exists=$(kc_get "/admin/realms/$KC_REALM/components?parent=$LDAP_ID&name=freeipa-groups" \
    | jq -r '.[0].id // empty')
if [[ -z "$exists" ]]; then
    kc_post "/admin/realms/$KC_REALM/components" "$GROUP_MAPPER" >/dev/null
    log "  mapper: freeipa-groups"
fi

# ─── Trigger initial sync ──────────────────────────────────────────────────────
log "Triggering initial user sync..."
SYNC_RESULT=$(kc_post "/admin/realms/$KC_REALM/user-storage/$LDAP_ID/sync?action=triggerFullSync" "" 2>/dev/null || echo "{}")
ADDED=$(echo "$SYNC_RESULT" | jq -r '.added // 0')
UPDATED=$(echo "$SYNC_RESULT" | jq -r '.updated // 0')
log "Sync complete: $ADDED added, $UPDATED updated."

# ─── Enable email login ────────────────────────────────────────────────────────
kc_put "/admin/realms/$KC_REALM" \
    '{"loginWithEmailAllowed":true,"duplicateEmailsAllowed":false}' >/dev/null
log "Email login enabled on realm '$KC_REALM'."

# ─── Summary ─────────────────────────────────────────────────────────────────
cat <<EOF

${GREEN}Keycloak ↔ FreeIPA configuration complete.${NC}

  Keycloak URL:   $KC_URL
  Realm:          $KC_REALM  (display: $KC_REALM_DISPLAY)
  LDAP provider:  $IPA_LDAP_URL
  Users DN:       cn=users,cn=accounts,$IPA_BASEDN
  Groups DN:      cn=groups,cn=accounts,$IPA_BASEDN
  Sync schedule:  full=${SYNC_FULL_PERIOD}s / changed=${SYNC_CHANGED_PERIOD}s

  Admin console:  $KC_URL/admin/$KC_REALM/console
  User login:     $KC_URL/realms/$KC_REALM/account

  Next steps:
  • Verify users are visible: Admin console → Users
  • Set up client applications (OIDC/SAML) in this realm
  • For production: switch Keycloak to 'start' mode with a TLS cert
  • For Kerberos/SPNEGO: supply an HTTP service keytab and set
    allowKerberosAuthentication=true in the LDAP provider config

EOF
