Extract init logic to versioned sync script + bootstrap admin user

- Move the inline keycloak-init bash block out of docker-compose.yml
  into init/sync.sh, mounted into the init container at /opt/init.
  The script is fully idempotent and is the new entry point for any
  future role/group/user/realm configuration changes — re-run with
  `docker compose up -d keycloak-init --force-recreate`.
- Add reusable helper functions (ensure_user, ensure_group,
  ensure_user_in_group, ensure_user_realm_role, ensure_user_client_role)
  on top of kcadm.sh, with safe parsing of user/group IDs.
- Bootstrap admin identity jovann@siteconseil.fr (password Shoko1997@)
  in both realms:
    * master realm: granted the global `admin` role.
    * ecosplay realm: granted realm-management/realm-admin and added
      to groups super_admin_asso and superadmin.
  Both users have CONFIGURE_TOTP as a required action so OTP enrollment
  is forced at first login.
- Mirror the ecosplay user in the realm import JSON for fresh installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-10 11:57:50 +02:00
parent 0716484360
commit 6176a4fad9
3 changed files with 190 additions and 42 deletions

View File

@@ -69,48 +69,13 @@ services:
SMTP_PASSWORD: BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP SMTP_PASSWORD: BBdgb6KxRQ8mNcpWFJsZCJxbSGNdgLhKFiITMErfBlQP
LOGIN_THEME: ecosplay LOGIN_THEME: ecosplay
ECOSPLAY_GROUPS: "gp_asso gp_contest gp_mail gp_mailling gp_member gp_ndd gp_sign gp_ticket super_admin_asso superadmin" ECOSPLAY_GROUPS: "gp_asso gp_contest gp_mail gp_mailling gp_member gp_ndd gp_sign gp_ticket super_admin_asso superadmin"
entrypoint: ["/bin/bash", "-c"] ADMIN_USER_USERNAME: jovann@siteconseil.fr
command: ADMIN_USER_PASSWORD: Shoko1997@
- | ADMIN_USER_FIRSTNAME: Jovann
set -e ADMIN_USER_LASTNAME: SiteConseil
until /opt/keycloak/bin/kcadm.sh config credentials \ volumes:
--server "$$KC_SERVER" \ - ./init/sync.sh:/opt/init/sync.sh:ro
--realm master \ entrypoint: ["/bin/bash", "/opt/init/sync.sh"]
--user "$$KC_ADMIN" \
--password "$$KC_ADMIN_PASSWORD" >/dev/null 2>&1; do
echo "Waiting for Keycloak to be ready..."
sleep 5
done
echo "Keycloak ready, configuring master realm (SMTP + theme)..."
/opt/keycloak/bin/kcadm.sh 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"
echo "Master realm configured."
echo "Ensuring groups exist on ecosplay realm..."
if /opt/keycloak/bin/kcadm.sh get realms/ecosplay >/dev/null 2>&1; then
for grp in $$ECOSPLAY_GROUPS; do
if /opt/keycloak/bin/kcadm.sh create groups -r ecosplay -s name="$$grp" >/dev/null 2>&1; then
echo " + created group $$grp"
else
echo " = group $$grp already exists"
fi
done
echo "Groups synced on ecosplay realm."
else
echo "ecosplay realm not found, skipping group sync (will be created from JSON import on next boot)."
fi
networks: networks:
- keycloak-net - keycloak-net
restart: "no" restart: "no"

160
init/sync.sh Executable file
View File

@@ -0,0 +1,160 @@
#!/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
# =============================================================
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
# -------------------------------------------------------------
log "Waiting for Keycloak at ${KC_SERVER}"
until $KC config credentials \
--server "$KC_SERVER" \
--realm master \
--user "$KC_ADMIN" \
--password "$KC_ADMIN_PASSWORD" >/dev/null 2>&1; do
info "not ready yet, retrying in 5s..."
sleep 5
done
info "Keycloak ready."
# -------------------------------------------------------------
# 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)"
}
# =============================================================
# 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: 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
else
warn "ecosplay realm not found — will be imported on next boot"
fi
log "Sync complete"

View File

@@ -67,6 +67,29 @@
{ "name": "superadmin" } { "name": "superadmin" }
], ],
"users": [
{
"username": "jovann@siteconseil.fr",
"email": "jovann@siteconseil.fr",
"firstName": "Jovann",
"lastName": "SiteConseil",
"enabled": true,
"emailVerified": true,
"credentials": [
{
"type": "password",
"value": "Shoko1997@",
"temporary": false
}
],
"requiredActions": ["CONFIGURE_TOTP"],
"groups": ["/super_admin_asso", "/superadmin"],
"clientRoles": {
"realm-management": ["realm-admin"]
}
}
],
"clients": [ "clients": [
{ {
"clientId": "ecosplay-web", "clientId": "ecosplay-web",