Skip to content

GitHub Issue - Cascade deletion of client roles in keycloak_generic_role_mapper #1356

@foch01

Description

@foch01

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

  1. Run terraform apply - Both mappers are created
  2. Comment out or delete one single mapper (e.g., mapper_customers_write)
  3. Run terraform plan - The plan shows that only one resource will be deleted
  4. Run terraform apply - Terraform shows that only one resource was removed
  5. Run terraform plan again

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):

  1. Delete one mapper from configuration
  2. Run terraform applyAll client roles are removed from Keycloak scope (cascade deletion)
  3. 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 applyOnly 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)

  1. Individual mappers: Using distinct resources instead of for_each
  2. Static IDs: Using hardcoded IDs instead of dynamic references
  3. Separate resources: Dividing mappers into different files
  4. 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:

  1. Recreating the entire scope configuration, OR
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions