The Gitea OAuth2 provider name changed from esy_lock to ecosplay_code, so the callback path follows: /user/oauth2/ecosplay_code/callback Update both the realm import JSON and sync.sh reconciliation for code.e-cosplay.fr and cos.local. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
359 lines
14 KiB
Bash
Executable File
359 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================
|
|
# E-Cosplay Keycloak — idempotent sync script
|
|
# =============================================================
|
|
# Run by the keycloak-init container on every `docker compose up`.
|
|
# Fully idempotent: re-running only applies missing config.
|
|
#
|
|
# Re-sync after editing this file:
|
|
# docker compose up -d keycloak-init --force-recreate
|
|
#
|
|
# Required env vars (set in docker-compose.yml):
|
|
# KC_SERVER, KC_ADMIN, KC_ADMIN_PASSWORD
|
|
# SMTP_HOST, SMTP_PORT, SMTP_FROM, SMTP_FROM_DISPLAY_NAME,
|
|
# SMTP_USER, SMTP_PASSWORD
|
|
# LOGIN_THEME
|
|
# ECOSPLAY_GROUPS (space-separated list)
|
|
# ADMIN_USER_USERNAME, ADMIN_USER_PASSWORD,
|
|
# ADMIN_USER_FIRSTNAME, ADMIN_USER_LASTNAME
|
|
# SYNC_BOT_CLIENT, SYNC_BOT_SECRET
|
|
# =============================================================
|
|
|
|
set -euo pipefail
|
|
|
|
KC=/opt/keycloak/bin/kcadm.sh
|
|
|
|
log() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; }
|
|
info() { printf ' %s\n' "$*"; }
|
|
warn() { printf ' \033[1;33m! %s\033[0m\n' "$*"; }
|
|
|
|
# -------------------------------------------------------------
|
|
# Wait for Keycloak and authenticate
|
|
# -------------------------------------------------------------
|
|
# Two authentication paths:
|
|
# 1. sync-bot service account (preferred, used after bootstrap).
|
|
# 2. Default bootstrap admin (used on first run, before sync-bot
|
|
# has been created and granted the admin role).
|
|
# -------------------------------------------------------------
|
|
KC_LOGIN_MODE=none
|
|
|
|
login_as_sync_bot() {
|
|
[ -n "${SYNC_BOT_SECRET:-}" ] || return 1
|
|
$KC config credentials \
|
|
--server "$KC_SERVER" \
|
|
--realm master \
|
|
--client "$SYNC_BOT_CLIENT" \
|
|
--secret "$SYNC_BOT_SECRET" >/dev/null 2>&1
|
|
}
|
|
|
|
login_as_admin() {
|
|
$KC config credentials \
|
|
--server "$KC_SERVER" \
|
|
--realm master \
|
|
--user "$KC_ADMIN" \
|
|
--password "$KC_ADMIN_PASSWORD" >/dev/null 2>&1
|
|
}
|
|
|
|
log "Authenticating to Keycloak at ${KC_SERVER}"
|
|
while true; do
|
|
if login_as_sync_bot; then
|
|
KC_LOGIN_MODE=sync-bot
|
|
break
|
|
fi
|
|
if login_as_admin; then
|
|
KC_LOGIN_MODE=bootstrap
|
|
break
|
|
fi
|
|
info "Keycloak not ready / no valid credentials yet, retrying in 5s..."
|
|
sleep 5
|
|
done
|
|
info "Authenticated (mode=$KC_LOGIN_MODE)"
|
|
|
|
# -------------------------------------------------------------
|
|
# Helpers
|
|
# -------------------------------------------------------------
|
|
realm_exists() {
|
|
$KC get "realms/$1" >/dev/null 2>&1
|
|
}
|
|
|
|
user_id() {
|
|
# $1 = realm, $2 = username
|
|
$KC get users -r "$1" -q username="$2" --fields id 2>/dev/null \
|
|
| sed -n 's/.*"id"[ ]*:[ ]*"\([^"]*\)".*/\1/p' \
|
|
| head -n1
|
|
}
|
|
|
|
group_id() {
|
|
# $1 = realm, $2 = group name (top level only)
|
|
$KC get "group-by-path/$2" -r "$1" --fields id 2>/dev/null \
|
|
| sed -n 's/.*"id"[ ]*:[ ]*"\([^"]*\)".*/\1/p' \
|
|
| head -n1
|
|
}
|
|
|
|
ensure_group() {
|
|
# $1 = realm, $2 = group name
|
|
if [ -n "$(group_id "$1" "$2")" ]; then
|
|
info "= group $2 ($1)"
|
|
else
|
|
$KC create groups -r "$1" -s name="$2" >/dev/null
|
|
info "+ group $2 ($1)"
|
|
fi
|
|
}
|
|
|
|
ensure_user() {
|
|
# $1=realm $2=username $3=password $4=firstname $5=lastname
|
|
if [ -n "$(user_id "$1" "$2")" ]; then
|
|
info "= user $2 ($1)"
|
|
return
|
|
fi
|
|
$KC create users -r "$1" \
|
|
-s username="$2" \
|
|
-s email="$2" \
|
|
-s firstName="$4" \
|
|
-s lastName="$5" \
|
|
-s enabled=true \
|
|
-s emailVerified=true \
|
|
-s 'requiredActions=["CONFIGURE_TOTP"]' >/dev/null
|
|
$KC set-password -r "$1" --username "$2" --new-password "$3"
|
|
info "+ user $2 ($1)"
|
|
}
|
|
|
|
ensure_user_in_group() {
|
|
# $1=realm $2=username $3=group
|
|
local uid gid
|
|
uid=$(user_id "$1" "$2")
|
|
gid=$(group_id "$1" "$3")
|
|
if [ -z "$uid" ]; then warn "skip group bind: user $2 missing in $1"; return; fi
|
|
if [ -z "$gid" ]; then warn "skip group bind: group $3 missing in $1"; return; fi
|
|
$KC update "users/$uid/groups/$gid" -r "$1" -n >/dev/null 2>&1 || true
|
|
info " $2 -> group $3 ($1)"
|
|
}
|
|
|
|
ensure_user_realm_role() {
|
|
# $1=realm $2=username $3=role
|
|
$KC add-roles -r "$1" --uusername "$2" --rolename "$3" >/dev/null 2>&1 || true
|
|
info " $2 -> realm role $3 ($1)"
|
|
}
|
|
|
|
ensure_user_client_role() {
|
|
# $1=realm $2=username $3=client $4=role
|
|
$KC add-roles -r "$1" --uusername "$2" --cclientid "$3" --rolename "$4" >/dev/null 2>&1 || true
|
|
info " $2 -> client role $3/$4 ($1)"
|
|
}
|
|
|
|
client_internal_id() {
|
|
# $1=realm $2=clientId
|
|
$KC get clients -r "$1" -q clientId="$2" --fields id 2>/dev/null \
|
|
| sed -n 's/.*"id"[ ]*:[ ]*"\([^"]*\)".*/\1/p' \
|
|
| head -n1
|
|
}
|
|
|
|
rename_client() {
|
|
# $1=realm $2=oldClientId $3=newClientId
|
|
local cid
|
|
cid=$(client_internal_id "$1" "$2")
|
|
if [ -n "$cid" ]; then
|
|
$KC update "clients/$cid" -r "$1" -s "clientId=$3" >/dev/null
|
|
info " renamed client $2 -> $3 ($1)"
|
|
fi
|
|
}
|
|
|
|
set_client_uris() {
|
|
# $1=realm $2=clientId $3=redirectUris(json) $4=webOrigins(json) $5=postLogoutUris(##-separated)
|
|
local cid
|
|
cid=$(client_internal_id "$1" "$2")
|
|
if [ -z "$cid" ]; then
|
|
warn "client $2 not found in $1, skipping URI sync"
|
|
return
|
|
fi
|
|
$KC update "clients/$cid" -r "$1" \
|
|
-s "redirectUris=$3" \
|
|
-s "webOrigins=$4" \
|
|
-s "attributes.\"post.logout.redirect.uris\"=$5" >/dev/null
|
|
info " client $2 URIs synced ($1)"
|
|
}
|
|
|
|
ensure_client() {
|
|
# $1=realm $2=clientId $3=name $4=description $5=secret
|
|
# $6=redirectUris(json) $7=webOrigins(json) $8=postLogoutUris(##-separated)
|
|
if [ -n "$(client_internal_id "$1" "$2")" ]; then
|
|
info "= client $2 ($1)"
|
|
return
|
|
fi
|
|
$KC create clients -r "$1" \
|
|
-s "clientId=$2" \
|
|
-s "name=$3" \
|
|
-s "description=$4" \
|
|
-s 'protocol=openid-connect' \
|
|
-s 'enabled=true' \
|
|
-s 'publicClient=false' \
|
|
-s "secret=$5" \
|
|
-s 'standardFlowEnabled=true' \
|
|
-s 'implicitFlowEnabled=false' \
|
|
-s 'directAccessGrantsEnabled=false' \
|
|
-s 'serviceAccountsEnabled=false' \
|
|
-s 'frontchannelLogout=true' \
|
|
-s "redirectUris=$6" \
|
|
-s "webOrigins=$7" \
|
|
-s "attributes.\"post.logout.redirect.uris\"=$8" \
|
|
-s 'attributes."pkce.code.challenge.method"=S256' >/dev/null
|
|
info "+ client $2 ($1)"
|
|
}
|
|
|
|
ensure_sync_bot() {
|
|
# Create (or keep in sync) the automation service client in master.
|
|
# Grants its service account the master 'admin' realm role so that
|
|
# sync.sh can authenticate with client_credentials after the default
|
|
# admin user is disabled.
|
|
local cid
|
|
cid=$(client_internal_id master "$SYNC_BOT_CLIENT")
|
|
if [ -z "$cid" ]; then
|
|
$KC create clients -r master \
|
|
-s "clientId=$SYNC_BOT_CLIENT" \
|
|
-s "name=Sync Bot" \
|
|
-s "description=Service account used by init/sync.sh for unattended config reconciliation" \
|
|
-s 'protocol=openid-connect' \
|
|
-s 'enabled=true' \
|
|
-s 'publicClient=false' \
|
|
-s "secret=$SYNC_BOT_SECRET" \
|
|
-s 'standardFlowEnabled=false' \
|
|
-s 'implicitFlowEnabled=false' \
|
|
-s 'directAccessGrantsEnabled=false' \
|
|
-s 'serviceAccountsEnabled=true' >/dev/null
|
|
cid=$(client_internal_id master "$SYNC_BOT_CLIENT")
|
|
info "+ service client $SYNC_BOT_CLIENT (master)"
|
|
else
|
|
$KC update "clients/$cid" -r master \
|
|
-s 'enabled=true' \
|
|
-s 'serviceAccountsEnabled=true' \
|
|
-s 'standardFlowEnabled=false' \
|
|
-s 'directAccessGrantsEnabled=false' \
|
|
-s "secret=$SYNC_BOT_SECRET" >/dev/null
|
|
info "= service client $SYNC_BOT_CLIENT (master)"
|
|
fi
|
|
# Grant 'admin' realm role to the auto-generated service account user
|
|
$KC add-roles -r master \
|
|
--uusername "service-account-${SYNC_BOT_CLIENT}" \
|
|
--rolename admin >/dev/null 2>&1 || true
|
|
info " service-account-${SYNC_BOT_CLIENT} -> realm role admin"
|
|
}
|
|
|
|
disable_default_admin() {
|
|
# Only disable once we're sure sync-bot can authenticate (otherwise
|
|
# next run would be locked out).
|
|
if ! login_as_sync_bot; then
|
|
warn "sync-bot auth not working, refusing to disable default admin"
|
|
login_as_admin >/dev/null 2>&1 || true
|
|
return
|
|
fi
|
|
# Re-login as admin to perform the disable action (sync-bot's token
|
|
# was used only for verification)
|
|
login_as_admin >/dev/null 2>&1 || true
|
|
|
|
local admin_id current
|
|
admin_id=$(user_id master "$KC_ADMIN")
|
|
if [ -z "$admin_id" ]; then
|
|
info "no '$KC_ADMIN' user in master, nothing to disable"
|
|
return
|
|
fi
|
|
current=$($KC get "users/$admin_id" -r master --fields enabled 2>/dev/null \
|
|
| sed -n 's/.*"enabled"[ ]*:[ ]*\(true\|false\).*/\1/p' | head -n1)
|
|
if [ "$current" = "false" ]; then
|
|
info "= default admin user '$KC_ADMIN' already disabled"
|
|
return
|
|
fi
|
|
$KC update "users/$admin_id" -r master -s enabled=false >/dev/null
|
|
info "+ default admin user '$KC_ADMIN' disabled"
|
|
}
|
|
|
|
# =============================================================
|
|
# Master realm: SMTP, theme, locale
|
|
# =============================================================
|
|
log "Configuring master realm (SMTP, theme, locale)"
|
|
$KC update realms/master \
|
|
-s "smtpServer.host=${SMTP_HOST}" \
|
|
-s "smtpServer.port=${SMTP_PORT}" \
|
|
-s "smtpServer.from=${SMTP_FROM}" \
|
|
-s "smtpServer.fromDisplayName=${SMTP_FROM_DISPLAY_NAME}" \
|
|
-s "smtpServer.auth=true" \
|
|
-s "smtpServer.starttls=true" \
|
|
-s "smtpServer.ssl=false" \
|
|
-s "smtpServer.user=${SMTP_USER}" \
|
|
-s "smtpServer.password=${SMTP_PASSWORD}" \
|
|
-s "loginTheme=${LOGIN_THEME}" \
|
|
-s "internationalizationEnabled=true" \
|
|
-s 'supportedLocales=["fr"]' \
|
|
-s "defaultLocale=fr"
|
|
info "master realm updated"
|
|
|
|
# =============================================================
|
|
# Master realm: automation service account (sync-bot)
|
|
# =============================================================
|
|
log "Ensuring sync-bot service client in master realm"
|
|
ensure_sync_bot
|
|
|
|
# =============================================================
|
|
# Master realm: global Keycloak admin user
|
|
# =============================================================
|
|
log "Ensuring global Keycloak admin user in master realm"
|
|
ensure_user master "$ADMIN_USER_USERNAME" "$ADMIN_USER_PASSWORD" "$ADMIN_USER_FIRSTNAME" "$ADMIN_USER_LASTNAME"
|
|
ensure_user_realm_role master "$ADMIN_USER_USERNAME" admin
|
|
|
|
# =============================================================
|
|
# Ecosplay realm: groups + application admin user
|
|
# =============================================================
|
|
if realm_exists ecosplay; then
|
|
log "Ensuring groups on ecosplay realm"
|
|
for grp in $ECOSPLAY_GROUPS; do
|
|
ensure_group ecosplay "$grp"
|
|
done
|
|
|
|
log "Ensuring application admin user in ecosplay realm"
|
|
ensure_user ecosplay "$ADMIN_USER_USERNAME" "$ADMIN_USER_PASSWORD" "$ADMIN_USER_FIRSTNAME" "$ADMIN_USER_LASTNAME"
|
|
ensure_user_client_role ecosplay "$ADMIN_USER_USERNAME" realm-management realm-admin
|
|
ensure_user_in_group ecosplay "$ADMIN_USER_USERNAME" super_admin_asso
|
|
ensure_user_in_group ecosplay "$ADMIN_USER_USERNAME" superadmin
|
|
|
|
log "Reconciling ecosplay_web client"
|
|
# Legacy rename: dash -> underscore (if migrated from older import)
|
|
rename_client ecosplay ecosplay-web ecosplay_web
|
|
set_client_uris ecosplay ecosplay_web \
|
|
'["https://www.e-cosplay.fr/oauth/keycloak","https://cos.local/oauth/keycloak"]' \
|
|
'["https://www.e-cosplay.fr","https://cos.local"]' \
|
|
'https://www.e-cosplay.fr/*##https://cos.local/*'
|
|
|
|
log "Reconciling eticket client"
|
|
ensure_client ecosplay eticket "E-Ticket" \
|
|
"Application billetterie ticket.e-cosplay.fr" \
|
|
"change-me-in-admin-console" \
|
|
'["https://ticket.e-cosplay.fr/api/auth/login/sso/validate","https://cos.local/api/auth/login/sso/validate","https://ticket.e-cosplay.fr/connection/sso/check","https://cos.local/connection/sso/check"]' \
|
|
'["https://ticket.e-cosplay.fr","https://cos.local"]' \
|
|
'https://ticket.e-cosplay.fr/*##https://cos.local/*'
|
|
set_client_uris ecosplay eticket \
|
|
'["https://ticket.e-cosplay.fr/api/auth/login/sso/validate","https://cos.local/api/auth/login/sso/validate","https://ticket.e-cosplay.fr/connection/sso/check","https://cos.local/connection/sso/check"]' \
|
|
'["https://ticket.e-cosplay.fr","https://cos.local"]' \
|
|
'https://ticket.e-cosplay.fr/*##https://cos.local/*'
|
|
|
|
log "Reconciling ecosplay_code client"
|
|
ensure_client ecosplay ecosplay_code "E-Cosplay Code" \
|
|
"Forge de code (Gitea) - login SSO via ecosplay_code provider" \
|
|
"change-me-in-admin-console" \
|
|
'["https://code.e-cosplay.fr/user/oauth2/ecosplay_code/callback","https://cos.local/user/oauth2/ecosplay_code/callback"]' \
|
|
'["https://code.e-cosplay.fr","https://cos.local"]' \
|
|
'https://code.e-cosplay.fr/*##https://cos.local/*'
|
|
set_client_uris ecosplay ecosplay_code \
|
|
'["https://code.e-cosplay.fr/user/oauth2/ecosplay_code/callback","https://cos.local/user/oauth2/ecosplay_code/callback"]' \
|
|
'["https://code.e-cosplay.fr","https://cos.local"]' \
|
|
'https://code.e-cosplay.fr/*##https://cos.local/*'
|
|
else
|
|
warn "ecosplay realm not found — will be imported on next boot"
|
|
fi
|
|
|
|
# =============================================================
|
|
# Disable default bootstrap admin (only after sync-bot is proven)
|
|
# =============================================================
|
|
log "Disabling default bootstrap admin user"
|
|
disable_default_admin
|
|
|
|
log "Sync complete"
|