From 57073cc2195fae755b69590a265ce0b140d605ed Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 27 Mar 2026 16:57:02 +0100 Subject: [PATCH 1/6] update for new permission checking of notification module --- .env.template | 4 + docker-compose.yml | 5 + scripts/enable-backend.sh | 71 ++++++++ scripts/migrate-permission-check.sh | 253 ++++++++++++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100755 scripts/migrate-permission-check.sh diff --git a/.env.template b/.env.template index 422758d..d563dcb 100644 --- a/.env.template +++ b/.env.template @@ -42,6 +42,10 @@ REPLICATION_BACKEND_PUBLIC_KEY= # random secret used for token encryption (replication-backend) REPLICATION_BACKEND_JWT_SECRET= +# Keycloak service account for replication-backend permission checks +REPLICATION_BACKEND_KEYCLOAK_CLIENT_ID=aam-backend +REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET= + # Sentry configuration (app, replication-backend and aam-backend) SENTRY_DSN= SENTRY_DSN_REPLICATION_BACKEND= diff --git a/docker-compose.yml b/docker-compose.yml index 4aad234..80c094f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -66,6 +66,10 @@ services: SENTRY_ENABLED: ${SENTRY_ENABLED} SENTRY_INSTANCE_NAME: ${INSTANCE_NAME}.${INSTANCE_DOMAIN} SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT} + KEYCLOAK_ADMIN_BASE_URL: https://${KEYCLOAK_URL} + KEYCLOAK_REALM: ${INSTANCE_NAME} + KEYCLOAK_ADMIN_CLIENT_ID: ${REPLICATION_BACKEND_KEYCLOAK_CLIENT_ID:-aam-backend} + KEYCLOAK_ADMIN_CLIENT_SECRET: ${REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET} PORT: 5984 restart: unless-stopped profiles: @@ -81,6 +85,7 @@ services: - external_web depends_on: - couchdb-with-permissions + - replication-backend - aam-backend-service-db - rabbitmq volumes: diff --git a/scripts/enable-backend.sh b/scripts/enable-backend.sh index d3273e4..7ea8f40 100755 --- a/scripts/enable-backend.sh +++ b/scripts/enable-backend.sh @@ -51,6 +51,9 @@ RENDER_API_CLIENT_ID_DEV=$(bws secret -t "$BWS_ACCESS_TOKEN" get "b53d7a1d-220e- RENDER_API_CLIENT_SECRET_DEV=$(bws secret -t "$BWS_ACCESS_TOKEN" get "83a8e38b-fc22-461f-91a0-b22700712b62" | jq -r .value) SENTRY_AUTH_TOKEN=$(bws secret -t "$BWS_ACCESS_TOKEN" get "b9a3e1eb-3925-4ed6-93f4-b2270073c82c" | jq -r .value) SENTRY_DSN_BACKEND=$(bws secret -t "$BWS_ACCESS_TOKEN" get "a858a580-9643-4330-8667-b2270073d7a6" | jq -r .value) +KEYCLOAK_HOST=$(bws secret -t "$BWS_ACCESS_TOKEN" get "3db87144-76c9-4690-8f59-b22600c8c927" | jq -r .value) +KEYCLOAK_PASSWORD=$(bws secret -t "$BWS_ACCESS_TOKEN" get "c5f42f09-b1c8-43a8-ae75-b22600c8f2e5" | jq -r .value) +KEYCLOAK_USER=$(bws secret -t "$BWS_ACCESS_TOKEN" get "fbe4ba07-538d-49e2-92dd-b22600c8d9d2" | jq -r .value) chars=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 @@ -130,6 +133,68 @@ generate_password() { done } +getKeycloakToken() { + token=$(curl -s -L "https://$KEYCLOAK_HOST/realms/master/protocol/openid-connect/token" -H 'Content-Type: application/x-www-form-urlencoded' --data-urlencode username="$KEYCLOAK_USER" --data-urlencode password="$KEYCLOAK_PASSWORD" --data-urlencode grant_type=password --data-urlencode client_id=admin-cli) + token=${token#*\"access_token\":\"} + token=${token%%\"*} +} + +createKeycloakBackendClient() { + local realm="$1" + + getKeycloakToken + + # create the aam-backend client (confidential, service account enabled) + clientResponse=$(curl -s -D - -o /dev/null -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/clients" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "aam-backend", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "serviceAccountsEnabled": true, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "protocol": "openid-connect" + }') + + # extract client UUID from Location header + location=$(echo "$clientResponse" | grep -i "^location:") + clientUuid=$(echo "$location" | sed -n 's#.*\([a-f0-9]\{8\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{12\}\).*#\1#p') + + if [ -z "$clientUuid" ]; then + echo "ERROR: Failed to create aam-backend client in Keycloak realm '$realm'." + return 1 + fi + + echo "Created aam-backend client: $clientUuid" + + # get client secret + clientSecret=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/client-secret" \ + -H "Authorization: Bearer $token" | jq -r .value) + + # get the service account user + serviceAccountUserId=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/service-account-user" \ + -H "Authorization: Bearer $token" | jq -r .id) + + # get the realm-management client UUID + realmMgmtClientUuid=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients?clientId=realm-management" \ + -H "Authorization: Bearer $token" | jq -r '.[0].id') + + # get the manage-realm role from realm-management client + manageRealmRole=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$realmMgmtClientUuid/roles/manage-realm" \ + -H "Authorization: Bearer $token") + + # assign manage-realm role to the service account + curl -s -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/users/$serviceAccountUserId/role-mappings/clients/$realmMgmtClientUuid" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d "[$manageRealmRole]" + + echo "Assigned manage-realm role to aam-backend service account." +} + ############################## # script ############################## @@ -182,6 +247,8 @@ setEnv CRYPTO_CONFIGURATION_SECRET "$password" "$path/config/aam-backend-service setEnv SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI "https://keycloak.aam-digital.com/realms/$instance" "$path/config/aam-backend-service/application.env" setEnv SPRING_DATASOURCE_USERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" setEnv SPRING_DATASOURCE_PASSWORD "$(getVar "$path/.env" COUCHDB_PASSWORD)" "$path/config/aam-backend-service/application.env" +setEnv AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHUSERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" +setEnv AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHPASSWORD "$(getVar "$path/.env" COUCHDB_PASSWORD)" "$path/config/aam-backend-service/application.env" setEnv COUCHDBCLIENTCONFIGURATION_BASICAUTHUSERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" setEnv COUCHDBCLIENTCONFIGURATION_BASICAUTHPASSWORD "$(getVar "$path/.env" COUCHDB_PASSWORD)" "$path/config/aam-backend-service/application.env" setEnv SQSCLIENTCONFIGURATION_BASICAUTHUSERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" @@ -195,6 +262,10 @@ setEnv SENTRY_AUTH_TOKEN "$SENTRY_AUTH_TOKEN" "$path/config/aam-backend-service/ setEnv SENTRY_DSN "$SENTRY_DSN_BACKEND" "$path/config/aam-backend-service/application.env" setEnv SENTRY_SERVER_NAME "$instance.$DOMAIN" "$path/config/aam-backend-service/application.env" +# create aam-backend Keycloak client for permission checks +createKeycloakBackendClient "$instance" +setEnv REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET "$clientSecret" "$path/.env" + setEnv COMPOSE_PROFILES "full-stack" "$path/.env" (cd "$path" && docker compose up -d) diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh new file mode 100755 index 0000000..1d2dc54 --- /dev/null +++ b/scripts/migrate-permission-check.sh @@ -0,0 +1,253 @@ +#!/bin/bash + +# Migration script: adds Keycloak service account for replication-backend permission checks. +# +# For each instance: +# - Adds REPLICATION_BACKEND_KEYCLOAK_CLIENT_ID / SECRET to .env +# - For full-stack instances: creates the aam-backend Keycloak client with manage-realm role +# - For full-stack instances: ensures AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTH* vars exist in application.env +# +# Usage: +# ./migrate-permission-check.sh # migrate all instances +# ./migrate-permission-check.sh # migrate single instance +# +# Requires: BWS_ACCESS_TOKEN set in environment or setup.env + +set -uo pipefail + +baseDirectory="/var/docker" +source "$baseDirectory/ndb-setup/setup.env" + +############################## +# BWS secrets +############################## + +if [[ -z "${BWS_ACCESS_TOKEN:-}" ]]; then + echo "BWS_ACCESS_TOKEN is not set. Abort." + exit 1 +fi + +bws config server-base https://vault.bitwarden.eu + +KEYCLOAK_HOST=$(bws secret -t "$BWS_ACCESS_TOKEN" get "3db87144-76c9-4690-8f59-b22600c8c927" | jq -r .value) +KEYCLOAK_PASSWORD=$(bws secret -t "$BWS_ACCESS_TOKEN" get "c5f42f09-b1c8-43a8-ae75-b22600c8f2e5" | jq -r .value) +KEYCLOAK_USER=$(bws secret -t "$BWS_ACCESS_TOKEN" get "fbe4ba07-538d-49e2-92dd-b22600c8d9d2" | jq -r .value) + +############################## +# helpers +############################## + +getVar() { + local file="$1" + local var="$2" + grep "^$var=" "$file" 2>/dev/null | cut -d '=' -f2- || echo "" +} + +setEnv() { + local key="$1" + local value="$2" + local file="$3" + sed -i "s|^$key=.*|$key=$value|g" "$file" +} + +# Append a variable to a file if it does not already exist +ensureEnv() { + local key="$1" + local value="$2" + local file="$3" + if ! grep -q "^$key=" "$file" 2>/dev/null; then + echo "$key=$value" >> "$file" + echo " + added $key to $(basename "$file")" + fi +} + +getKeycloakToken() { + local raw + raw=$(curl -s -L "https://$KEYCLOAK_HOST/realms/master/protocol/openid-connect/token" \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode username="$KEYCLOAK_USER" \ + --data-urlencode password="$KEYCLOAK_PASSWORD" \ + --data-urlencode grant_type=password \ + --data-urlencode client_id=admin-cli) + token=${raw#*\"access_token\":\"} + token=${token%%\"*} + + if [ -z "$token" ] || [ "$token" = "$raw" ]; then + echo " ERROR: Failed to get Keycloak admin token." + return 1 + fi +} + +# Creates the aam-backend Keycloak client (if it doesn't exist) and assigns manage-realm role. +# Sets $clientSecret on success. +createOrGetKeycloakBackendClient() { + local realm="$1" + clientSecret="" + + getKeycloakToken + + # check if aam-backend client already exists + local existing + existing=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients?clientId=aam-backend" \ + -H "Authorization: Bearer $token") + local existingUuid + existingUuid=$(echo "$existing" | jq -r '.[0].id // empty') + + if [ -n "$existingUuid" ]; then + echo " aam-backend client already exists: $existingUuid" + clientSecret=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$existingUuid/client-secret" \ + -H "Authorization: Bearer $token" | jq -r '.value // empty') + + # ensure service account has manage-realm role (idempotent) + assignManageRealmRole "$realm" "$existingUuid" + return 0 + fi + + # create the aam-backend client (confidential, service account enabled) + local clientResponse + clientResponse=$(curl -s -D - -o /dev/null -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/clients" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "aam-backend", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "serviceAccountsEnabled": true, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "protocol": "openid-connect" + }') + + local location clientUuid + location=$(echo "$clientResponse" | grep -i "^location:") + clientUuid=$(echo "$location" | sed -n 's#.*\([a-f0-9]\{8\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{12\}\).*#\1#p') + + if [ -z "$clientUuid" ]; then + echo " ERROR: Failed to create aam-backend client in realm '$realm'." + return 1 + fi + + echo " Created aam-backend client: $clientUuid" + + clientSecret=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/client-secret" \ + -H "Authorization: Bearer $token" | jq -r '.value // empty') + + assignManageRealmRole "$realm" "$clientUuid" +} + +assignManageRealmRole() { + local realm="$1" + local aamBackendClientUuid="$2" + + local serviceAccountUserId + serviceAccountUserId=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$aamBackendClientUuid/service-account-user" \ + -H "Authorization: Bearer $token" | jq -r '.id // empty') + + if [ -z "$serviceAccountUserId" ]; then + echo " WARNING: Could not get service account user for aam-backend client." + return 1 + fi + + local realmMgmtClientUuid + realmMgmtClientUuid=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients?clientId=realm-management" \ + -H "Authorization: Bearer $token" | jq -r '.[0].id // empty') + + local manageRealmRole + manageRealmRole=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$realmMgmtClientUuid/roles/manage-realm" \ + -H "Authorization: Bearer $token") + + curl -s -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/users/$serviceAccountUserId/role-mappings/clients/$realmMgmtClientUuid" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d "[$manageRealmRole]" + + echo " Ensured manage-realm role on aam-backend service account." +} + +############################## +# migrate one instance +############################## + +migrate_instance() { + local instanceDir="$1" + local instance + instance=$(basename "$instanceDir") + instance=${instance#"$PREFIX"} + + local envFile="$instanceDir/.env" + local appEnvFile="$instanceDir/config/aam-backend-service/application.env" + + if [ ! -f "$envFile" ]; then + echo "[$instance] no .env file, skipping" + return + fi + + local profile + profile=$(getVar "$envFile" COMPOSE_PROFILES) + + # only full-stack instances need migration (permission check is only used by aam-backend-service) + if [ "$profile" != "full-stack" ]; then + echo "[$instance] profile=$profile — skipping (not full-stack)" + return + fi + + echo "[$instance] migrating..." + + # 1. Add Keycloak vars to .env (for replication-backend) + ensureEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_ID" "aam-backend" "$envFile" + ensureEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET" "" "$envFile" + + # 2. Create aam-backend Keycloak client (or get existing) + assign manage-realm + if createOrGetKeycloakBackendClient "$instance"; then + if [ -n "$clientSecret" ]; then + setEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET" "$clientSecret" "$envFile" + echo " Set REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET in .env" + else + echo " WARNING: Could not retrieve client secret" + fi + fi + + # 3. Ensure application.env has replication-backend basic auth vars + if [ -f "$appEnvFile" ]; then + local couchUser couchPass + couchUser=$(getVar "$envFile" COUCHDB_USER) + couchPass=$(getVar "$envFile" COUCHDB_PASSWORD) + + ensureEnv "AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHUSERNAME" "$couchUser" "$appEnvFile" + ensureEnv "AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHPASSWORD" "$couchPass" "$appEnvFile" + else + echo " no application.env found — skipping aam-backend-service config" + fi + + # 4. Restart + echo " Restarting..." + (cd "$instanceDir" && docker compose down && docker compose pull && docker compose up -d) + + echo "[$instance] done" + echo "" +} + +############################## +# main +############################## + +if [ -n "${1:-}" ]; then + # single instance mode + path="$baseDirectory/$PREFIX$1" + if [ ! -d "$path" ]; then + echo "Instance directory not found: $path" + exit 1 + fi + migrate_instance "$path" +else + # all instances + cd "$baseDirectory" + for D in ${PREFIX}*; do + if [ -d "$D" ]; then + migrate_instance "$baseDirectory/$D" + fi + done +fi + +echo "Migration complete." From 1669cc2e923e8d4ba716a1a33586cc070fc4d38f Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 27 Mar 2026 17:01:13 +0100 Subject: [PATCH 2/6] backup and update docker-compose also --- scripts/migrate-permission-check.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh index 1d2dc54..7e78b94 100755 --- a/scripts/migrate-permission-check.sh +++ b/scripts/migrate-permission-check.sh @@ -50,6 +50,16 @@ setEnv() { sed -i "s|^$key=.*|$key=$value|g" "$file" } +# Create a timestamped backup of a file (once per run, skip if backup already exists) +backupFile() { + local file="$1" + local backup="$file.bak-$(date +%Y%m%d%H%M%S)" + if [ -f "$file" ]; then + cp "$file" "$backup" + echo " backup: $(basename "$backup")" + fi +} + # Append a variable to a file if it does not already exist ensureEnv() { local key="$1" @@ -194,6 +204,15 @@ migrate_instance() { echo "[$instance] migrating..." + # backup files before any modification + backupFile "$envFile" + backupFile "$instanceDir/docker-compose.yml" + [ -f "$appEnvFile" ] && backupFile "$appEnvFile" + + # 0. Update docker-compose.yml from shared ndb-setup template + cp "$baseDirectory/ndb-setup/docker-compose.yml" "$instanceDir/docker-compose.yml" + echo " Updated docker-compose.yml from ndb-setup template" + # 1. Add Keycloak vars to .env (for replication-backend) ensureEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_ID" "aam-backend" "$envFile" ensureEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET" "" "$envFile" From ccd46127d049346d9eee7139508eacec29b838a6 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 27 Mar 2026 18:05:48 +0100 Subject: [PATCH 3/6] harden the scripts --- scripts/enable-backend.sh | 81 ++++++++++++++++++++--------- scripts/migrate-permission-check.sh | 8 ++- 2 files changed, 63 insertions(+), 26 deletions(-) diff --git a/scripts/enable-backend.sh b/scripts/enable-backend.sh index 7ea8f40..58502f9 100755 --- a/scripts/enable-backend.sh +++ b/scripts/enable-backend.sh @@ -141,46 +141,72 @@ getKeycloakToken() { createKeycloakBackendClient() { local realm="$1" + clientSecret="" getKeycloakToken - # create the aam-backend client (confidential, service account enabled) - clientResponse=$(curl -s -D - -o /dev/null -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/clients" \ - -H "Authorization: Bearer $token" \ - -H "Content-Type: application/json" \ - -d '{ - "clientId": "aam-backend", - "enabled": true, - "clientAuthenticatorType": "client-secret", - "serviceAccountsEnabled": true, - "publicClient": false, - "standardFlowEnabled": false, - "directAccessGrantsEnabled": false, - "protocol": "openid-connect" - }') - - # extract client UUID from Location header - location=$(echo "$clientResponse" | grep -i "^location:") - clientUuid=$(echo "$location" | sed -n 's#.*\([a-f0-9]\{8\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{12\}\).*#\1#p') + # check if aam-backend client already exists (idempotent) + local existing + existing=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients?clientId=aam-backend" \ + -H "Authorization: Bearer $token") + clientUuid=$(echo "$existing" | jq -r '.[0].id // empty') if [ -z "$clientUuid" ]; then - echo "ERROR: Failed to create aam-backend client in Keycloak realm '$realm'." - return 1 - fi + # create the aam-backend client (confidential, service account enabled) + clientResponse=$(curl -s -D - -o /dev/null -X POST "https://$KEYCLOAK_HOST/admin/realms/$realm/clients" \ + -H "Authorization: Bearer $token" \ + -H "Content-Type: application/json" \ + -d '{ + "clientId": "aam-backend", + "enabled": true, + "clientAuthenticatorType": "client-secret", + "serviceAccountsEnabled": true, + "publicClient": false, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false, + "protocol": "openid-connect" + }') + + # extract client UUID from Location header + location=$(echo "$clientResponse" | grep -i "^location:") + clientUuid=$(echo "$location" | sed -n 's#.*\([a-f0-9]\{8\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{4\}-[a-f0-9]\{12\}\).*#\1#p') + + if [ -z "$clientUuid" ]; then + echo "ERROR: Failed to create aam-backend client in Keycloak realm '$realm'." + return 1 + fi - echo "Created aam-backend client: $clientUuid" + echo "Created aam-backend client: $clientUuid" + else + echo "aam-backend client already exists: $clientUuid" + fi # get client secret clientSecret=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/client-secret" \ - -H "Authorization: Bearer $token" | jq -r .value) + -H "Authorization: Bearer $token" | jq -r '.value // empty') + + if [ -z "$clientSecret" ]; then + echo "ERROR: Failed to retrieve client secret for aam-backend client in realm '$realm'." + return 1 + fi # get the service account user serviceAccountUserId=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/service-account-user" \ - -H "Authorization: Bearer $token" | jq -r .id) + -H "Authorization: Bearer $token" | jq -r '.id // empty') + + if [ -z "$serviceAccountUserId" ]; then + echo "ERROR: Failed to retrieve service account user for aam-backend client in realm '$realm'." + return 1 + fi # get the realm-management client UUID realmMgmtClientUuid=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients?clientId=realm-management" \ - -H "Authorization: Bearer $token" | jq -r '.[0].id') + -H "Authorization: Bearer $token" | jq -r '.[0].id // empty') + + if [ -z "$realmMgmtClientUuid" ]; then + echo "ERROR: Failed to retrieve realm-management client in realm '$realm'." + return 1 + fi # get the manage-realm role from realm-management client manageRealmRole=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$realmMgmtClientUuid/roles/manage-realm" \ @@ -264,6 +290,11 @@ setEnv SENTRY_SERVER_NAME "$instance.$DOMAIN" "$path/config/aam-backend-service/ # create aam-backend Keycloak client for permission checks createKeycloakBackendClient "$instance" + +# ensure key exists before setting (older .env templates may lack it) +if ! grep -q '^REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET=' "$path/.env"; then + echo "REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET=" >> "$path/.env" +fi setEnv REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET "$clientSecret" "$path/.env" setEnv COMPOSE_PROFILES "full-stack" "$path/.env" diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh index 7e78b94..45aa0da 100755 --- a/scripts/migrate-permission-check.sh +++ b/scripts/migrate-permission-check.sh @@ -223,8 +223,14 @@ migrate_instance() { setEnv "REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET" "$clientSecret" "$envFile" echo " Set REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET in .env" else - echo " WARNING: Could not retrieve client secret" + echo " ERROR: Client created/fetched but secret could not be retrieved." + echo " Skipping restart for $instance to avoid broken permission-check config." + return 1 fi + else + echo " ERROR: Failed to create or get Keycloak backend client for $instance." + echo " Skipping restart for this instance to avoid broken permission-check config." + return 1 fi # 3. Ensure application.env has replication-backend basic auth vars From 020c2194f51d3d4249bf0613db7a69c7e258e6d1 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Fri, 27 Mar 2026 18:22:32 +0100 Subject: [PATCH 4/6] update env setup --- scripts/enable-backend.sh | 1 + scripts/migrate-permission-check.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/scripts/enable-backend.sh b/scripts/enable-backend.sh index 58502f9..161c847 100755 --- a/scripts/enable-backend.sh +++ b/scripts/enable-backend.sh @@ -273,6 +273,7 @@ setEnv CRYPTO_CONFIGURATION_SECRET "$password" "$path/config/aam-backend-service setEnv SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUERURI "https://keycloak.aam-digital.com/realms/$instance" "$path/config/aam-backend-service/application.env" setEnv SPRING_DATASOURCE_USERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" setEnv SPRING_DATASOURCE_PASSWORD "$(getVar "$path/.env" COUCHDB_PASSWORD)" "$path/config/aam-backend-service/application.env" +setEnv AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASEPATH "http://replication-backend:5984" "$path/config/aam-backend-service/application.env" setEnv AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHUSERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" setEnv AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHPASSWORD "$(getVar "$path/.env" COUCHDB_PASSWORD)" "$path/config/aam-backend-service/application.env" setEnv COUCHDBCLIENTCONFIGURATION_BASICAUTHUSERNAME "$(getVar "$path/.env" COUCHDB_USER)" "$path/config/aam-backend-service/application.env" diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh index 45aa0da..2e93355 100755 --- a/scripts/migrate-permission-check.sh +++ b/scripts/migrate-permission-check.sh @@ -239,6 +239,7 @@ migrate_instance() { couchUser=$(getVar "$envFile" COUCHDB_USER) couchPass=$(getVar "$envFile" COUCHDB_PASSWORD) + ensureEnv "AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASEPATH" "http://replication-backend:5984" "$appEnvFile" ensureEnv "AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHUSERNAME" "$couchUser" "$appEnvFile" ensureEnv "AAMREPLICATIONBACKENDCLIENTCONFIGURATION_BASICAUTHPASSWORD" "$couchPass" "$appEnvFile" else From c76760fc85157711f51b76a7c64a423d2d02c10c Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Mon, 30 Mar 2026 15:03:55 +0200 Subject: [PATCH 5/6] small fixes from review --- scripts/enable-backend.sh | 16 +++++++++++++--- scripts/migrate-permission-check.sh | 11 +++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/scripts/enable-backend.sh b/scripts/enable-backend.sh index 161c847..2e68056 100755 --- a/scripts/enable-backend.sh +++ b/scripts/enable-backend.sh @@ -104,9 +104,12 @@ setEnv() { local key="$1" local value="$2" local path="$3" + # escape sed special characters in value (\, &, |) + local escaped + escaped=$(printf '%s' "$value" | sed 's/[\\&|]/\\&/g') - sed -i "s|^$key=.*|$key=$value|g" "$path" # linux - # gsed -i "s|^$key=.*|$key=$value|g" "$path" # macos + sed -i "s|^$key=.*|$key=$escaped|g" "$path" # linux + # gsed -i "s|^$key=.*|$key=$escaped|g" "$path" # macos } # Funktion zum Abrufen der Umgebungsvariablen @@ -290,7 +293,14 @@ setEnv SENTRY_DSN "$SENTRY_DSN_BACKEND" "$path/config/aam-backend-service/applic setEnv SENTRY_SERVER_NAME "$instance.$DOMAIN" "$path/config/aam-backend-service/application.env" # create aam-backend Keycloak client for permission checks -createKeycloakBackendClient "$instance" +if ! createKeycloakBackendClient "$instance"; then + echo "ERROR: Failed to create/get Keycloak backend client for '$instance'. Aborting." + exit 1 +fi +if [ -z "$clientSecret" ]; then + echo "ERROR: Keycloak client created but secret could not be retrieved for '$instance'. Aborting." + exit 1 +fi # ensure key exists before setting (older .env templates may lack it) if ! grep -q '^REPLICATION_BACKEND_KEYCLOAK_CLIENT_SECRET=' "$path/.env"; then diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh index 2e93355..ff9bf87 100755 --- a/scripts/migrate-permission-check.sh +++ b/scripts/migrate-permission-check.sh @@ -47,10 +47,13 @@ setEnv() { local key="$1" local value="$2" local file="$3" - sed -i "s|^$key=.*|$key=$value|g" "$file" + # escape sed special characters in value (\, &, |) + local escaped + escaped=$(printf '%s' "$value" | sed 's/[\\&|]/\\&/g') + sed -i "s|^$key=.*|$key=$escaped|g" "$file" } -# Create a timestamped backup of a file (once per run, skip if backup already exists) +# Create a timestamped backup of a file backupFile() { local file="$1" local backup="$file.bak-$(date +%Y%m%d%H%M%S)" @@ -268,6 +271,10 @@ if [ -n "${1:-}" ]; then migrate_instance "$path" else # all instances + if [ -z "${PREFIX:-}" ]; then + echo "ERROR: PREFIX is not set. Aborting to avoid operating on all directories." + exit 1 + fi cd "$baseDirectory" for D in ${PREFIX}*; do if [ -d "$D" ]; then From 12d04c7d5e2fcf41644135f2f26fabfa57674453 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Mon, 30 Mar 2026 19:23:47 +0200 Subject: [PATCH 6/6] add missing keycloak client details --- scripts/enable-backend.sh | 13 +++++++++++++ scripts/migrate-permission-check.sh | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/scripts/enable-backend.sh b/scripts/enable-backend.sh index 2e68056..5cd02ab 100755 --- a/scripts/enable-backend.sh +++ b/scripts/enable-backend.sh @@ -222,6 +222,19 @@ createKeycloakBackendClient() { -d "[$manageRealmRole]" echo "Assigned manage-realm role to aam-backend service account." + + # ensure the "roles" client scope is assigned (required for role claims in the access token) + local rolesScopeUuid + rolesScopeUuid=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/client-scopes" \ + -H "Authorization: Bearer $token" | jq -r '.[] | select(.name == "roles") | .id // empty') + + if [ -n "$rolesScopeUuid" ]; then + curl -s -X PUT "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$clientUuid/default-client-scopes/$rolesScopeUuid" \ + -H "Authorization: Bearer $token" + echo "Ensured 'roles' client scope on aam-backend client." + else + echo "WARNING: Could not find 'roles' client scope in realm '$realm'." + fi } ############################## diff --git a/scripts/migrate-permission-check.sh b/scripts/migrate-permission-check.sh index ff9bf87..d31a4d8 100755 --- a/scripts/migrate-permission-check.sh +++ b/scripts/migrate-permission-check.sh @@ -176,6 +176,19 @@ assignManageRealmRole() { -d "[$manageRealmRole]" echo " Ensured manage-realm role on aam-backend service account." + + # ensure the "roles" client scope is assigned (required for role claims in the access token) + local rolesScopeUuid + rolesScopeUuid=$(curl -s -L "https://$KEYCLOAK_HOST/admin/realms/$realm/client-scopes" \ + -H "Authorization: Bearer $token" | jq -r '.[] | select(.name == "roles") | .id // empty') + + if [ -n "$rolesScopeUuid" ]; then + curl -s -X PUT "https://$KEYCLOAK_HOST/admin/realms/$realm/clients/$aamBackendClientUuid/default-client-scopes/$rolesScopeUuid" \ + -H "Authorization: Bearer $token" + echo " Ensured 'roles' client scope on aam-backend client." + else + echo " WARNING: Could not find 'roles' client scope in realm '$realm'." + fi } ##############################