From 8ced919e00aabd5027c86696a244602ddefa85f3 Mon Sep 17 00:00:00 2001 From: Danijel Simeunovic Date: Fri, 24 Apr 2026 08:40:57 +0200 Subject: [PATCH] new --- .../backstage-keycloak-client-config.yaml | 4 ++ docs/REFERENCE.md | 8 +-- infra/values/base/backstage-values.yaml | 6 +-- infra/values/base/keycloak-values.yaml | 53 ++++++++++++++++++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/cluster-resources/backstage-keycloak-client-config.yaml b/cluster-resources/backstage-keycloak-client-config.yaml index 7bfd34f..58f8418 100644 --- a/cluster-resources/backstage-keycloak-client-config.yaml +++ b/cluster-resources/backstage-keycloak-client-config.yaml @@ -14,6 +14,10 @@ stringData: { "clientId": "backstage", "name": "Backstage Developer Portal", + "serviceAccountsEnabled": true, + "serviceAccountRoles": { + "realm-management": ["view-users", "query-users", "view-groups", "query-groups"] + }, "redirectUris": ["https://backstage.forteapps.net/api/auth/oidc/handler/frame"], "webOrigins": ["https://backstage.forteapps.net"], "defaultClientScopes": ["openid", "email", "profile"], diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 4cfca12..c2320de 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -997,6 +997,7 @@ ignore: - **Important**: `dangerouslyAllowSignInWithoutUserInCatalog` must be nested inside the resolver object, not at the provider level **Keycloak User/Group Sync**: +- The `backstage` Keycloak client has `serviceAccountsEnabled: true` with `realm-management` roles (`view-users`, `query-users`, `view-groups`, `query-groups`) — assigned automatically by the registrar - The `keycloakOrg` catalog provider auto-imports users and groups from the `forte` realm - Requires the Keycloak dynamic plugin to be enabled (pre-installed but disabled by default in RHDH) - Syncs every 30 minutes with 15-second initial delay @@ -1082,9 +1083,10 @@ upstream: 2. For each config Secret, parses `client.json` and computes a config hash 3. Skips if hash matches annotation and credential Secret already exists 4. Creates or updates the Keycloak client via Admin API -5. Fetches the generated client secret -6. Upserts credential Secret in target namespace + central `secrets` namespace -7. Annotates config Secret with sync status, config hash, and timestamp +5. If `serviceAccountsEnabled: true` and `serviceAccountRoles` defined, assigns service account roles (e.g., `realm-management` → `view-users`) +6. Fetches the generated client secret +7. Upserts credential Secret in target namespace + central `secrets` namespace +8. Annotates config Secret with sync status, config hash, and timestamp **Resources**: - `ServiceAccount`: `keycloak-client-registrar` (namespace: `keycloak`) diff --git a/infra/values/base/backstage-values.yaml b/infra/values/base/backstage-values.yaml index 3474277..98c7fdb 100644 --- a/infra/values/base/backstage-values.yaml +++ b/infra/values/base/backstage-values.yaml @@ -22,9 +22,9 @@ route: upstream: backstage: image: - registry: quay.io - repository: rhdh-community/rhdh - tag: next + registry: ghcr.io + repository: vfarcic/idp-full-backstage + tag: 0.0.3 podSecurityContext: runAsUser: 1001 diff --git a/infra/values/base/keycloak-values.yaml b/infra/values/base/keycloak-values.yaml index 18d8b37..fd9b818 100644 --- a/infra/values/base/keycloak-values.yaml +++ b/infra/values/base/keycloak-values.yaml @@ -371,7 +371,7 @@ extraDeploy: continue fi - # Build Keycloak client representation (strip our secret delivery config) + # Build Keycloak client representation (strip our secret/role delivery config) KC_CLIENT=$(echo "$CLIENT_JSON" | jq '{ clientId: .clientId, name: .name, @@ -379,6 +379,7 @@ extraDeploy: protocol: "openid-connect", clientAuthenticatorType: "client-secret", standardFlowEnabled: true, + serviceAccountsEnabled: (.serviceAccountsEnabled // false), directAccessGrantsEnabled: false, publicClient: false, redirectUris: .redirectUris, @@ -426,6 +427,56 @@ extraDeploy: # Sync credentials to target namespace sync_credentials "$CLIENT_ID" "$CLIENT_UUID" "$CRED_NS" "$CRED_NAME" "$CRED_ID_KEY" "$CRED_SECRET_KEY" + # Assign service account roles if serviceAccountsEnabled + SA_ENABLED=$(echo "$CLIENT_JSON" | jq -r '.serviceAccountsEnabled // false') + SA_ROLES_JSON=$(echo "$CLIENT_JSON" | jq -c '.serviceAccountRoles // empty') + if [ "$SA_ENABLED" = "true" ] && [ -n "$SA_ROLES_JSON" ]; then + echo " Assigning service account roles for '${CLIENT_ID}'" + # Get the service account user for this client + SA_USER_ID=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${CLIENT_UUID}/service-account-user" \ + | jq -r '.id // empty') + if [ -z "$SA_USER_ID" ]; then + echo " WARNING: Could not get service account user for '${CLIENT_ID}'" + else + # Iterate over each target client and its roles + echo "$SA_ROLES_JSON" | jq -r 'keys[]' | while read -r TARGET_CLIENT_ID; do + # Get the target client's UUID + TARGET_UUID=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=${TARGET_CLIENT_ID}" \ + | jq -r '.[0].id // empty') + if [ -z "$TARGET_UUID" ]; then + echo " WARNING: Target client '${TARGET_CLIENT_ID}' not found" + continue + fi + # Get available roles from the target client + AVAILABLE_ROLES=$(curl -sf -H "Authorization: Bearer ${TOKEN}" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${TARGET_UUID}/roles") + # Build the role payload from requested role names + ROLE_PAYLOAD=$(echo "$SA_ROLES_JSON" | jq -c --arg tc "$TARGET_CLIENT_ID" --argjson avail "$AVAILABLE_ROLES" ' + .[$tc] as $wanted | + [$avail[] | select(.name as $n | $wanted | index($n))] + ') + ROLE_COUNT=$(echo "$ROLE_PAYLOAD" | jq 'length') + if [ "$ROLE_COUNT" = "0" ]; then + echo " WARNING: No matching roles found for '${TARGET_CLIENT_ID}'" + continue + fi + # Assign the roles to the service account + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -X POST -d "$ROLE_PAYLOAD" \ + "${KEYCLOAK_URL}/admin/realms/${REALM}/users/${SA_USER_ID}/role-mappings/clients/${TARGET_UUID}") + if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then + echo " Assigned ${ROLE_COUNT} roles from '${TARGET_CLIENT_ID}'" + else + echo " WARNING: Failed to assign roles from '${TARGET_CLIENT_ID}' (HTTP ${HTTP_CODE})" + fi + done + fi + fi + # Annotate config Secret with hash and sync status annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/config-hash" "$CONFIG_HASH" annotate_secret "keycloak" "$CONFIG_NAME" "keycloak.forteapps.net/sync-status" "synced"