-
Notifications
You must be signed in to change notification settings - Fork 377
Description
Describe the bug
Version
- Keycloak Provider: v5.5.0 (latest tested version)
- Keycloak Version: Test in version 26.0.5. The issue is the same in 26.3.4 because the API takes an
optional body. - Terraform: 1.13.4
Problem
Deleting a client role via a keycloak_generic_role_mapper resource causes the deletion of all client roles from the scope instead of just one.
When client A has multiple roles from client B assigned to its scope, deleting one of them with Terraform causes the deletion of all other roles from the same scope.
This problem is identical to the one described in issue #838 which concerned realm roles and was fixed in version v4.1.0.
Suspected Root Cause
According to the analysis of the provider's source code (role_scope_mapping.go), when deleting a client role from the scope, the Keycloak API expects to receive the roles in the request body as an array:
DELETE /{realm}/clients/{id}/scope-mappings/clients/{client}
However, in the DeleteRoleScopeMapping function, for client roles (role.ClientRole = true), the request is sent without a body:
func (keycloakClient *KeycloakClient) DeleteRoleScopeMapping(ctx context.Context, realmId string, clientId string, clientScopeId string, role *Role) error {
roleUrl := roleScopeMappingUrl(realmId, clientId, clientScopeId, role)
if role.ClientRole {
return keycloakClient.delete(ctx, roleUrl, nil) // ← Empty body (nil)
} else {
body := [1]RealmRoleRepresentation{
// ... body with realm role data
}
return keycloakClient.delete(ctx, roleUrl, body)
}
}Steps to Reproduce
1. Terraform Configuration
# Define a client that owns the roles
resource "keycloak_openid_client" "backend_client" {
realm_id = "public"
client_id = "api-backend"
access_type = "CONFIDENTIAL"
service_accounts_enabled = true
}
# Create multiple client roles
resource "keycloak_role" "backend_role_1" {
realm_id = "public"
client_id = keycloak_openid_client.backend_client.id
name = "customers:read"
}
resource "keycloak_role" "backend_role_2" {
realm_id = "public"
client_id = keycloak_openid_client.backend_client.id
name = "customers:write"
}
# Define a client scope
resource "keycloak_openid_client_scope" "app_client_roles" {
realm_id = "public"
name = "app-client-roles"
description = "Application client roles scope"
include_in_token_scope = false
}
# Add roles to scope via individual mappers
resource "keycloak_generic_role_mapper" "mapper_customers_read" {
realm_id = "public"
client_scope_id = keycloak_openid_client_scope.app_client_roles.id
role_id = keycloak_role.backend_role_1.id
}
resource "keycloak_generic_role_mapper" "mapper_customers_write" {
realm_id = "public"
client_scope_id = keycloak_openid_client_scope.app_client_roles.id
role_id = keycloak_role.backend_role_2.id
}2. Reproduction Steps
- Run
terraform apply- Both mappers are created - Comment out or delete one single mapper (e.g.,
mapper_customers_write) - Run
terraform plan- The plan shows that only one resource will be deleted - Run
terraform apply- Terraform shows that only one resource was removed - Run
terraform planagain
3. Observed vs Expected Behavior
Expected Behavior:
- The plan should show 0 changes since only one mapper was deleted
Observed Behavior:
- The plan shows that all mappers need to be recreated:
Plan: 2 to add, 0 to change, 0 to destroy.
# keycloak_generic_role_mapper.mapper_customers_read will be created
+ resource "keycloak_generic_role_mapper" "mapper_customers_read" {
+ client_scope_id = "d9c667a4-827e-44fa-8c88-32675e51a7b0"
+ id = (known after apply)
+ realm_id = "public"
+ role_id = "d3bdccb1-4b5c-4a14-8e96-394ceeaf8f1e"
}
# keycloak_generic_role_mapper.mapper_customers_write will be created
+ resource "keycloak_generic_role_mapper" "mapper_customers_write" {
+ client_scope_id = "d9c667a4-827e-44fa-8c88-32675e51a7b0"
+ id = (known after apply)
+ realm_id = "public"
+ role_id = "f6fb5915-d026-4ded-b517-a87ae9abc9e5"
}
4. Complete Issue Cycle - Two Apply Required
Important: To achieve the intended behavior (having only the unwanted role removed), you need to perform two terraform apply cycles:
First Apply Cycle (Cascade Deletion):
- Delete one mapper from configuration
- Run
terraform apply→ All client roles are removed from Keycloak scope (cascade deletion) - Terraform shows only 1 resource deleted (misleading output)
Second Apply Cycle (Correction):
4. Run terraform plan → Shows all remaining mappers need to be recreated
5. Run terraform apply → Only the intended mappers are recreated
6. The originally deleted mapper is correctly absent
This reveals the core issues:
- The deletion operation removes all client roles from the scope instead of just one
- Terraform state becomes out of sync with Keycloak reality
- A second apply is required to restore the desired state
- The process is unpredictable and requires manual intervention
This two-step workaround demonstrates that the provider's deletion logic is fundamentally broken for client roles.
Tests Performed
Workarounds Tested (Unsuccessful)
- Individual mappers: Using distinct resources instead of
for_each - Static IDs: Using hardcoded IDs instead of dynamic references
- Separate resources: Dividing mappers into different files
- Provider update: Testing with the latest version (v5.5.0)
Problem Confirmation
- ✅ Role assignment works correctly
- ✅ Assignment via Keycloak interface works
- ❌ Deleting one client role deletes all roles from the scope
- ✅ Roles from other clients are not affected
- ✅ Terraform plan correctly shows a single deletion but the actual effect is different
- ✅ Two apply cycles are required to achieve the intended result
Impact
This bug prevents granular management of client roles in scopes, making it impossible to individually remove permissions without:
- Recreating the entire scope configuration, OR
- Performing a two-step apply process (unpredictable and error-prone)
Proposed Solution
Fix the DeleteRoleScopeMapping function for client roles by following the same pattern as realm roles:
func (keycloakClient *KeycloakClient) DeleteRoleScopeMapping(ctx context.Context, realmId string, clientId string, clientScopeId string, role *Role) error {
roleUrl := roleScopeMappingUrl(realmId, clientId, clientScopeId, role)
body := [1]RealmRoleRepresentation{
{
Id: role.Id,
Name: role.Name,
Description: role.Description,
Composite: role.Composite,
ClientRole: role.ClientRole,
ContainerId: role.ContainerId,
},
}
return keycloakClient.delete(ctx, roleUrl, body)
}Version
Test in version 26.0.5. The issue is the same in 26.3.4 because the API takes an optional body
Expected behavior
No response
Actual behavior
No response
How to Reproduce?
No response
Anything else?
No response