Repository: mrparkers/terraform-provider-keycloak
Pull request #331 (2020-06-28 22:36:14)

refactor keycloak_group_roles implementation, add missing docs for keycloak_user_roles


Patch:
diff --git a/docs/resources/keycloak_user_roles.md b/docs/resources/keycloak_user_roles.md
new file mode 100644
index 0000000..f46dc0f
--- /dev/null
+++ b/docs/resources/keycloak_user_roles.md
@@ -0,0 +1,88 @@
+# keycloak_user_roles
+
+Allows you to manage roles assigned to a Keycloak user.
+
+Note that this resource attempts to be an **authoritative** source over
+user roles. When this resource takes control over a user's roles,
+roles that are manually assigned to the user will be removed, and roles
+that are manually removed from the user will be assigned upon the next run
+of `terraform apply`.
+
+Note that when assigning composite roles to a user, you may see a
+non-empty plan following a `terraform apply` if you assign a role and a
+composite that includes that role to the same user.
+
+### Example Usage
+
+```hcl
+resource "keycloak_realm" "realm" {
+  realm   = "my-realm"
+  enabled = true
+}
+
+resource "keycloak_role" "realm_role" {
+  realm_id    = "${keycloak_realm.realm.id}"
+  name        = "my-realm-role"
+  description = "My Realm Role"
+}
+
+resource "keycloak_openid_client" "client" {
+  realm_id  = "${keycloak_realm.realm.id}"
+  client_id = "client"
+  name      = "client"
+
+  enabled = true
+
+  access_type = "BEARER-ONLY"
+}
+
+resource "keycloak_role" "client_role" {
+  realm_id    = "${keycloak_realm.realm.id}"
+  client_id   = "${keycloak_client.client.id}"
+  name        = "my-client-role"
+  description = "My Client Role"
+}
+
+resource "keycloak_user" "user" {
+    realm_id   = "${keycloak_realm.realm.id}"
+    username   = "bob"
+    enabled    = true
+
+    email      = "bob@domain.com"
+    first_name = "Bob"
+    last_name  = "Bobson"
+}
+
+resource "keycloak_user_roles" "user_roles" {
+  realm_id = "${keycloak_realm.realm.id}"
+  user_id = "${keycloak_user.user.id}"
+
+  role_ids = [
+    "${keycloak_role.realm_role.id}",
+    "${keycloak_role.client_role.id}",
+  ]
+}
+```
+
+### Argument Reference
+
+The following arguments are supported:
+
+- `realm_id` - (Required) The realm this user exists in.
+- `user_id` - (Required) The ID of the user this resource should
+  manage roles for.
+- `role_ids` - (Required) A list of role IDs to map to the user
+
+### Import
+
+This resource can be imported using the format
+`{{realm_id}}/{{user_id}}`, where `user_id` is the unique ID that
+Keycloak assigns to the user upon creation. This value can be found in
+the GUI when editing the user, and is typically a GUID.
+
+Example:
+
+```bash
+$ terraform import keycloak_user_roles.user_roles my-realm/b0ae6924-1bd5-4655-9e38-dae7c5e42924
+```
+
diff --git a/example/roles.tf b/example/roles.tf
index 613bc49..0722f7f 100644
--- a/example/roles.tf
+++ b/example/roles.tf
@@ -184,7 +184,7 @@ data "keycloak_role" "realm_offline_access" {
   name     = "offline_access"
 }
 
-resource "keycloak_group_roles" "admin_roles" {
+resource "keycloak_group_roles" "admin_group_roles" {
   realm_id = keycloak_realm.roles_example.id
   group_id = keycloak_group.pet_api_admins.id
 
@@ -208,3 +208,25 @@ resource "keycloak_group_roles" "front_desk_roles" {
     data.keycloak_role.realm_offline_access.id,
   ]
 }
+
+resource "keycloak_user" "admin_user" {
+  realm_id = keycloak_realm.roles_example.id
+  username = "admin-user"
+
+  email      = "admin-user@fakedomain.com"
+  first_name = "Admin"
+  last_name  = "Istrator"
+}
+
+resource "keycloak_user_roles" "admin_user_roles" {
+  realm_id = keycloak_realm.roles_example.id
+  user_id  = keycloak_user.admin_user.id
+
+  role_ids = [
+    keycloak_role.pet_api_read_pet.id,
+    keycloak_role.pet_api_read_pet_details.id,
+    keycloak_role.pet_api_delete_pet.id,
+    keycloak_role.pet_api_create_pet.id,
+    data.keycloak_role.realm_offline_access.id,
+  ]
+}
diff --git a/keycloak/group.go b/keycloak/group.go
index e4d2998..69b7a91 100644
--- a/keycloak/group.go
+++ b/keycloak/group.go
@@ -227,27 +227,3 @@ func (keycloakClient *KeycloakClient) GetDefaultGroups(realmName string) ([]Grou
 
 	return defaultGroups, err
 }
-
-func (keycloakClient *KeycloakClient) AddRealmRolesToGroup(realmId, groupId string, roles []*Role) error {
-	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles)
-
-	return err
-}
-
-func (keycloakClient *KeycloakClient) AddClientRolesToGroup(realmId, groupId, clientId string, roles []*Role) error {
-	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles)
-
-	return err
-}
-
-func (keycloakClient *KeycloakClient) RemoveRealmRolesFromGroup(realmId, groupId string, roles []*Role) error {
-	err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles)
-
-	return err
-}
-
-func (keycloakClient *KeycloakClient) RemoveClientRolesFromGroup(realmId, groupId, clientId string, roles []*Role) error {
-	err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles)
-
-	return err
-}
diff --git a/keycloak/group_role_mappings.go b/keycloak/group_role_mappings.go
new file mode 100644
index 0000000..7cb4f05
--- /dev/null
+++ b/keycloak/group_role_mappings.go
@@ -0,0 +1,37 @@
+package keycloak
+
+import "fmt"
+
+func (keycloakClient *KeycloakClient) GetGroupRoleMappings(realmId string, userId string) (*RoleMapping, error) {
+	var roleMapping *RoleMapping
+	err := keycloakClient.get(fmt.Sprintf("/realms/%s/groups/%s/role-mappings", realmId, userId), &roleMapping, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return roleMapping, nil
+}
+
+func (keycloakClient *KeycloakClient) AddRealmRolesToGroup(realmId, groupId string, roles []*Role) error {
+	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles)
+
+	return err
+}
+
+func (keycloakClient *KeycloakClient) AddClientRolesToGroup(realmId, groupId, clientId string, roles []*Role) error {
+	_, _, err := keycloakClient.post(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles)
+
+	return err
+}
+
+func (keycloakClient *KeycloakClient) RemoveRealmRolesFromGroup(realmId, groupId string, roles []*Role) error {
+	err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/realm", realmId, groupId), roles)
+
+	return err
+}
+
+func (keycloakClient *KeycloakClient) RemoveClientRolesFromGroup(realmId, groupId, clientId string, roles []*Role) error {
+	err := keycloakClient.delete(fmt.Sprintf("/realms/%s/groups/%s/role-mappings/clients/%s", realmId, groupId, clientId), roles)
+
+	return err
+}
diff --git a/keycloak/role_mapping_types.go b/keycloak/role_mapping_types.go
new file mode 100644
index 0000000..fe74897
--- /dev/null
+++ b/keycloak/role_mapping_types.go
@@ -0,0 +1,16 @@
+package keycloak
+
+// struct for the MappingRepresentation
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_mappingsrepresentation
+type RoleMapping struct {
+	ClientMappings map[string]*ClientRoleMapping `json:"clientMappings"`
+	RealmMappings  []*Role                       `json:"realmMappings"`
+}
+
+// struct for the ClientMappingRepresentation
+// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientmappingsrepresentation
+type ClientRoleMapping struct {
+	Client   string  `json:"client"`
+	Id       string  `json:"id"`
+	Mappings []*Role `json:"mappings"`
+}
diff --git a/keycloak/user_role_mappings.go b/keycloak/user_role_mappings.go
index ac90da8..ce1e8dd 100644
--- a/keycloak/user_role_mappings.go
+++ b/keycloak/user_role_mappings.go
@@ -2,23 +2,8 @@ package keycloak
 
 import "fmt"
 
-// struct for the MappingRepresentation
-// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_mappingsrepresentation
-type UserRoleMapping struct {
-	ClientMappings map[string]*ClientRoleMapping `json:"clientMappings"`
-	RealmMappings  []*Role                       `json:"realmMappings"`
-}
-
-// struct for the ClientMappingRepresentation
-// https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientmappingsrepresentation
-type ClientRoleMapping struct {
-	Client   string  `json:"client"`
-	Id       string  `json:"id"`
-	Mappings []*Role `json:"mappings"`
-}
-
-func (keycloakClient *KeycloakClient) GetUserRoleMappings(realmId string, userId string) (*UserRoleMapping, error) {
-	var roleMapping *UserRoleMapping
+func (keycloakClient *KeycloakClient) GetUserRoleMappings(realmId string, userId string) (*RoleMapping, error) {
+	var roleMapping *RoleMapping
 	err := keycloakClient.get(fmt.Sprintf("/realms/%s/users/%s/role-mappings", realmId, userId), &roleMapping, nil)
 	if err != nil {
 		return nil, err
diff --git a/mkdocs.yml b/mkdocs.yml
index eed61c2..6881a7d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -13,8 +13,9 @@ nav:
 - Resources:
   - keycloak_realm: resources/keycloak_realm.md
   - keycloak_realm_events: resources/keycloak_realm_events.md
-  - keycloak_user: resources/keycloak_user.md
   - keycloak_role: resources/keycloak_role.md
+  - keycloak_user: resources/keycloak_user.md
+  - keycloak_user_roles: resources/keycloak_user_roles.md
   - keycloak_group: resources/keycloak_group.md
   - keycloak_group_memberships: resources/keycloak_group_memberships.md
   - keycloak_group_roles: resources/keycloak_group_roles.md
diff --git a/provider/resource_keycloak_group_roles.go b/provider/resource_keycloak_group_roles.go
index 1b89113..d0335be 100644
--- a/provider/resource_keycloak_group_roles.go
+++ b/provider/resource_keycloak_group_roles.go
@@ -9,9 +9,9 @@ import (
 
 func resourceKeycloakGroupRoles() *schema.Resource {
 	return &schema.Resource{
-		Create: resourceKeycloakGroupRolesCreate,
+		Create: resourceKeycloakGroupRolesReconcile,
 		Read:   resourceKeycloakGroupRolesRead,
-		Update: resourceKeycloakGroupRolesUpdate,
+		Update: resourceKeycloakGroupRolesReconcile,
 		Delete: resourceKeycloakGroupRolesDelete,
 		// This resource can be imported using {{realm}}/{{groupId}}.
 		Importer: &schema.ResourceImporter{
@@ -42,142 +42,47 @@ func groupRolesId(realmId, groupId string) string {
 	return fmt.Sprintf("%s/%s", realmId, groupId)
 }
 
-func getMapOfRealmAndClientRoles(keycloakClient *keycloak.KeycloakClient, realmId string, roleIds []string) (map[string][]*keycloak.Role, error) {
-	roles := make(map[string][]*keycloak.Role)
-
-	for _, roleId := range roleIds {
-		role, err := keycloakClient.GetRole(realmId, roleId)
+func addRolesToGroup(keycloakClient *keycloak.KeycloakClient, clientRolesToAdd map[string][]*keycloak.Role, realmRolesToAdd []*keycloak.Role, group *keycloak.Group) error {
+	if len(realmRolesToAdd) != 0 {
+		err := keycloakClient.AddRealmRolesToGroup(group.RealmId, group.Id, realmRolesToAdd)
 		if err != nil {
-			return nil, err
-		}
-
-		if role.ClientRole {
-			roles[role.ClientId] = append(roles[role.ClientId], role)
-		} else {
-			roles["realm"] = append(roles["realm"], role)
-		}
-	}
-
-	return roles, nil
-}
-
-// given a group and a map of roles we already know about, fetch the roles we don't know about
-// `localRoles` is used as a cache to avoid unnecessary http requests
-func getMapOfRealmAndClientRolesFromGroup(keycloakClient *keycloak.KeycloakClient, group *keycloak.Group, localRoles map[string][]*keycloak.Role) (map[string][]*keycloak.Role, error) {
-	roles := make(map[string][]*keycloak.Role)
-
-	// realm roles
-	if len(group.RealmRoles) != 0 {
-		var realmRoles []*keycloak.Role
-
-		for _, realmRoleName := range group.RealmRoles {
-			found := false
-
-			for _, localRealmRole := range localRoles["realm"] {
-				if localRealmRole.Name == realmRoleName {
-					found = true
-					realmRoles = append(realmRoles, localRealmRole)
-
-					break
-				}
-			}
-
-			if !found {
-				realmRole, err := keycloakClient.GetRoleByName(group.RealmId, "", realmRoleName)
-				if err != nil {
-					return nil, err
-				}
-
-				realmRoles = append(realmRoles, realmRole)
-			}
+			return err
 		}
-
-		roles["realm"] = realmRoles
 	}
 
-	// client roles
-	if len(group.ClientRoles) != 0 {
-		for clientName, clientRoleNames := range group.ClientRoles {
-			client, err := keycloakClient.GetGenericClientByClientId(group.RealmId, clientName)
+	for k, roles := range clientRolesToAdd {
+		if len(roles) != 0 {
+			err := keycloakClient.AddClientRolesToGroup(group.RealmId, group.Id, k, roles)
 			if err != nil {
-				return nil, err
-			}
-
-			var clientRoles []*keycloak.Role
-			for _, clientRoleName := range clientRoleNames {
-				found := false
-
-				for _, localClientRole := range localRoles[client.Id] {
-					if localClientRole.Name == clientRoleName {
-						found = true
-						clientRoles = append(clientRoles, localClientRole)
-
-						break
-					}
-				}
-
-				if !found {
-					clientRole, err := keycloakClient.GetRoleByName(group.RealmId, client.Id, clientRoleName)
-					if err != nil {
-						return nil, err
-					}
-
-					clientRoles = append(clientRoles, clientRole)
-				}
+				return err
 			}
-
-			roles[client.Id] = clientRoles
-		}
-	}
-
-	return roles, nil
-}
-
-func addRolesToGroup(keycloakClient *keycloak.KeycloakClient, rolesToAdd map[string][]*keycloak.Role, group *keycloak.Group) error {
-	if realmRoles, ok := rolesToAdd["realm"]; ok && len(realmRoles) != 0 {
-		err := keycloakClient.AddRealmRolesToGroup(group.RealmId, group.Id, realmRoles)
-		if err != nil {
-			return err
-		}
-	}
-
-	for k, roles := range rolesToAdd {
-		if k == "realm" {
-			continue
-		}
-
-		err := keycloakClient.AddClientRolesToGroup(group.RealmId, group.Id, k, roles)
-		if err != nil {
-			return err
 		}
 	}
 
 	return nil
 }
 
-func removeRolesFromGroup(keycloakClient *keycloak.KeycloakClient, rolesToRemove map[string][]*keycloak.Role, group *keycloak.Group) error {
-	if realmRoles, ok := rolesToRemove["realm"]; ok && len(realmRoles) != 0 {
-		err := keycloakClient.RemoveRealmRolesFromGroup(group.RealmId, group.Id, realmRoles)
+func removeRolesFromGroup(keycloakClient *keycloak.KeycloakClient, clientRolesToRemove map[string][]*keycloak.Role, realmRolesToRemove []*keycloak.Role, group *keycloak.Group) error {
+	if len(realmRolesToRemove) != 0 {
+		err := keycloakClient.RemoveRealmRolesFromGroup(group.RealmId, group.Id, realmRolesToRemove)
 		if err != nil {
 			return err
 		}
 	}
 
-	for k, roles := range rolesToRemove {
-		if k == "realm" {
-			continue
-		}
-
-		err := keycloakClient.RemoveClientRolesFromGroup(group.RealmId, group.Id, k, roles)
-		if err != nil {
-			return err
+	for k, roles := range clientRolesToRemove {
+		if len(roles) != 0 {
+			err := keycloakClient.RemoveClientRolesFromGroup(group.RealmId, group.Id, k, roles)
+			if err != nil {
+				return err
+			}
 		}
 	}
 
 	return nil
 }
 
-func resourceKeycloakGroupRolesCreate(data *schema.ResourceData, meta interface{}) error {
+func resourceKeycloakGroupRolesReconcile(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
 	realmId := data.Get("realm_id").(string)
@@ -189,18 +94,30 @@ func resourceKeycloakGroupRolesCreate(data *schema.ResourceData, meta interface{
 	}
 
 	roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List())
-	rolesToAdd, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds)
+	tfRoles, err := getExtendedRoleMapping(keycloakClient, realmId, roleIds)
 	if err != nil {
 		return err
 	}
 
-	err = addRolesToGroup(keycloakClient, rolesToAdd, group)
+	// get the list of currently assigned roles. Due to default realm and client roles
+	roleMappings, err := keycloakClient.GetGroupRoleMappings(realmId, groupId)
+
+	// sort into roles we need to add and roles we need to remove
+	updates := calculateRoleMappingUpdates(tfRoles, intoRoleMapping(roleMappings))
+
+	// add roles
+	err = addRolesToGroup(keycloakClient, updates.clientRolesToAdd, updates.realmRolesToAdd, group)
 	if err != nil {
 		return err
 	}
 
-	data.SetId(groupRolesId(realmId, groupId))
+	// remove roles
+	err = removeRolesFromGroup(keycloakClient, updates.clientRolesToRemove, updates.realmRolesToRemove, group)
+	if err != nil {
+		return err
+	}
 
+	data.SetId(groupRolesId(realmId, groupId))
 	return resourceKeycloakGroupRolesRead(data, meta)
 }
 
@@ -210,39 +127,20 @@ func resourceKeycloakGroupRolesRead(data *schema.ResourceData, meta interface{})
 	realmId := data.Get("realm_id").(string)
 	groupId := data.Get("group_id").(string)
 
-	group, err := keycloakClient.GetGroup(realmId, groupId)
+	roles, err := keycloakClient.GetGroupRoleMappings(realmId, groupId)
 	if err != nil {
 		return err
 	}
 
 	var roleIds []string
 
-	if len(group.RealmRoles) != 0 {
-		for _, realmRole := range group.RealmRoles {
-			role, err := keycloakClient.GetRoleByName(realmId, "", realmRole)
-			if err != nil {
-				return err
-			}
-
-			roleIds = append(roleIds, role.Id)
-		}
+	for _, realmRole := range roles.RealmMappings {
+		roleIds = append(roleIds, realmRole.Id)
 	}
 
-	if len(group.ClientRoles) != 0 {
-		for clientName, clientRoles := range group.ClientRoles {
-			client, err := keycloakClient.GetGenericClientByClientId(realmId, clientName)
-			if err != nil {
-				return err
-			}
-
-			for _, clientRole := range clientRoles {
-				role, err := keycloakClient.GetRoleByName(realmId, client.Id, clientRole)
-				if err != nil {
-					return err
-				}
-
-				roleIds = append(roleIds, role.Id)
-			}
+	for _, clientRoleMapping := range roles.ClientMappings {
+		for _, clientRole := range clientRoleMapping.Mappings {
+			roleIds = append(roleIds, clientRole.Id)
 		}
 	}
 
@@ -252,47 +150,6 @@ func resourceKeycloakGroupRolesRead(data *schema.ResourceData, meta interface{})
 	return nil
 }
 
-func resourceKeycloakGroupRolesUpdate(data *schema.ResourceData, meta interface{}) error {
-	keycloakClient := meta.(*keycloak.KeycloakClient)
-
-	realmId := data.Get("realm_id").(string)
-	groupId := data.Get("group_id").(string)
-
-	group, err := keycloakClient.GetGroup(realmId, groupId)
-	if err != nil {
-		return err
-	}
-
-	roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List())
-
-	tfRoles, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds)
-	if err != nil {
-		return err
-	}
-
-	remoteRoles, err := getMapOfRealmAndClientRolesFromGroup(keycloakClient, group, tfRoles)
-	if err != nil {
-		return err
-	}
-
-	removeDuplicateRoles(&tfRoles, &remoteRoles)
-
-	// `tfRoles` contains all roles that need to be added
-	// `remoteRoles` contains all roles that need to be removed
-
-	err = addRolesToGroup(keycloakClient, tfRoles, group)
-	if err != nil {
-		return err
-	}
-
-	err = removeRolesFromGroup(keycloakClient, remoteRoles, group)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func resourceKeycloakGroupRolesDelete(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
@@ -302,12 +159,12 @@ func resourceKeycloakGroupRolesDelete(data *schema.ResourceData, meta interface{
 	group, err := keycloakClient.GetGroup(realmId, groupId)
 
 	roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List())
-	rolesToRemove, err := getMapOfRealmAndClientRoles(keycloakClient, realmId, roleIds)
+	rolesToRemove, err := getExtendedRoleMapping(keycloakClient, realmId, roleIds)
 	if err != nil {
 		return err
 	}
 
-	err = removeRolesFromGroup(keycloakClient, rolesToRemove, group)
+	err = removeRolesFromGroup(keycloakClient, rolesToRemove.clientRoles, rolesToRemove.realmRoles, group)
 	if err != nil {
 		return err
 	}
@@ -329,28 +186,3 @@ func resourceKeycloakGroupRolesImport(d *schema.ResourceData, _ interface{}) ([]
 
 	return []*schema.ResourceData{d}, nil
 }
-
-func removeRoleFromSlice(slice []*keycloak.Role, index int) []*keycloak.Role {
-	slice[index] = slice[len(slice)-1]
-	return slice[:len(slice)-1]
-}
-
-func removeDuplicateRoles(one, two *map[string][]*keycloak.Role) {
-	for k := range *one {
-		for i1 := 0; i1 < len((*one)[k]); i1++ {
-			s1 := (*one)[k][i1]
-
-			for i2 := 0; i2 < len((*two)[k]); i2++ {
-				s2 := (*two)[k][i2]
-
-				if s1.Id == s2.Id {
-					(*one)[k] = removeRoleFromSlice((*one)[k], i1)
-					(*two)[k] = removeRoleFromSlice((*two)[k], i2)
-
-					i1--
-					break
-				}
-			}
-		}
-	}
-}
diff --git a/provider/resource_keycloak_user_roles.go b/provider/resource_keycloak_user_roles.go
index 39e368b..436fe39 100644
--- a/provider/resource_keycloak_user_roles.go
+++ b/provider/resource_keycloak_user_roles.go
@@ -9,9 +9,9 @@ import (
 
 func resourceKeycloakUserRoles() *schema.Resource {
 	return &schema.Resource{
-		Create: resourceKeycloakUserRolesCreate,
+		Create: resourceKeycloakUserRolesReconcile,
 		Read:   resourceKeycloakUserRolesRead,
-		Update: resourceKeycloakUserRolesUpdate,
+		Update: resourceKeycloakUserRolesReconcile,
 		Delete: resourceKeycloakUserRolesDelete,
 		// This resource can be imported using {{realm}}/{{userId}}.
 		Importer: &schema.ResourceImporter{
@@ -82,7 +82,7 @@ func removeRolesFromUser(keycloakClient *keycloak.KeycloakClient, clientRolesToR
 	return nil
 }
 
-func resourceKeycloakUserRolesCreate(data *schema.ResourceData, meta interface{}) error {
+func resourceKeycloakUserRolesReconcile(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
 	realmId := data.Get("realm_id").(string)
@@ -99,8 +99,8 @@ func resourceKeycloakUserRolesCreate(data *schema.ResourceData, meta interface{}
 		return err
 	}
 
-	// get the list of currently assigned roles. Due to default-realm- and client-roles
-	// (e.g. roles of the account-client) this is probably not empty upon resource creation
+	// get the list of currently assigned roles. Due to default realm and client roles
+	// (e.g. roles of the account client) this is probably not empty upon resource creation
 	roleMappings, err := keycloakClient.GetUserRoleMappings(realmId, userId)
 
 	// sort into roles we need to add and roles we need to remove
@@ -151,46 +151,6 @@ func resourceKeycloakUserRolesRead(data *schema.ResourceData, meta interface{})
 	return nil
 }
 
-func resourceKeycloakUserRolesUpdate(data *schema.ResourceData, meta interface{}) error {
-	keycloakClient := meta.(*keycloak.KeycloakClient)
-
-	realmId := data.Get("realm_id").(string)
-	userId := data.Get("user_id").(string)
-
-	user, err := keycloakClient.GetUser(realmId, userId)
-	if err != nil {
-		return err
-	}
-
-	roleIds := interfaceSliceToStringSlice(data.Get("role_ids").(*schema.Set).List())
-	tfRoles, err := getExtendedRoleMapping(keycloakClient, realmId, roleIds)
-	if err != nil {
-		return err
-	}
-
-	roleMappings, err := keycloakClient.GetUserRoleMappings(realmId, userId)
-	if err != nil {
-		return err
-	}
-
-	updates := calculateRoleMappingUpdates(tfRoles, intoRoleMapping(roleMappings))
-
-	// `tfRoles` contains all roles that need to be added
-	// `remoteRoles` contains all roles that need to be removed
-
-	err = addRolesToUser(keycloakClient, updates.clientRolesToAdd, updates.realmRolesToAdd, user)
-	if err != nil {
-		return err
-	}
-
-	err = removeRolesFromUser(keycloakClient, updates.clientRolesToRemove, updates.realmRolesToRemove, user)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
 func resourceKeycloakUserRolesDelete(data *schema.ResourceData, meta interface{}) error {
 	keycloakClient := meta.(*keycloak.KeycloakClient)
 
@@ -227,121 +187,3 @@ func resourceKeycloakUserRolesImport(d *schema.ResourceData, _ interface{}) ([]*
 
 	return []*schema.ResourceData{d}, nil
 }
-
-// a struct that represents the "desired" state configured via terraform
-// the key for 'clientRoles' is keycloak's client-id (the uuid, not to be confused with the OAuth Client Id)
-type roleMapping struct {
-	clientRoles map[string][]*keycloak.Role
-	realmRoles  []*keycloak.Role
-}
-
-// transform the keycloak response UserRoleMapping into the internal roleMapping
-// so we can compare the current state (keycloak) against the desired state (terraform)
-func intoRoleMapping(userRoleMapping *keycloak.UserRoleMapping) *roleMapping {
-	clientRoles := make(map[string][]*keycloak.Role)
-	for _, clientRoleMapping := range userRoleMapping.ClientMappings {
-		clientRoles[clientRoleMapping.Id] = clientRoleMapping.Mappings
-	}
-
-	mapping := roleMapping{
-		clientRoles: clientRoles,
-		realmRoles:  userRoleMapping.RealmMappings,
-	}
-
-	return &mapping
-}
-
-// given a list or roleIds, query keycloak for role-details to find out if a role is a client-role or a
-// realm-rule (which is required to POST the role-assignment to the correct API-endpoint)
-func getExtendedRoleMapping(keycloakClient *keycloak.KeycloakClient, realmId string, roleIds []string) (*roleMapping, error) {
-	clientRoles := make(map[string][]*keycloak.Role)
-	var realmRoles []*keycloak.Role
-
-	for _, roleId := range roleIds {
-		role, err := keycloakClient.GetRole(realmId, roleId)
-		if err != nil {
-			return nil, err
-		}
-
-		if role.ClientRole {
-			clientRoles[role.ClientId] = append(clientRoles[role.ClientId], role)
-		} else {
-			realmRoles = append(realmRoles, role)
-		}
-	}
-
-	mapping := roleMapping{
-		clientRoles: clientRoles,
-		realmRoles:  realmRoles,
-	}
-
-	return &mapping, nil
-}
-
-type terraformRoleMappingUpdates struct {
-	realmRolesToRemove  []*keycloak.Role
-	realmRolesToAdd     []*keycloak.Role
-	clientRolesToRemove map[string][]*keycloak.Role
-	clientRolesToAdd    map[string][]*keycloak.Role
-}
-
-// given the existing roles (queried from keycloak) and the requested roles (via tf)
-// calculate the required updates, i.e. roles to remove and roles to add
-func calculateRoleMappingUpdates(requestedRoles *roleMapping, existingRoles *roleMapping) *terraformRoleMappingUpdates {
-	clientRolesToRemove := make(map[string][]*keycloak.Role)
-	clientRolesToAdd := make(map[string][]*keycloak.Role)
-
-	realmRolesToRemove := minusRoles(existingRoles.realmRoles, requestedRoles.realmRoles)
-	realmRolesToAdd := minusRoles(requestedRoles.realmRoles, existingRoles.realmRoles)
-
-	for clientId, requestedClientRoles := range requestedRoles.clientRoles {
-		if existingClientRoles, ok := existingRoles.clientRoles[clientId]; ok {
-			clientRolesToAdd[clientId] = minusRoles(requestedClientRoles, existingClientRoles)
-			clientRolesToRemove[clientId] = minusRoles(existingClientRoles, requestedClientRoles)
-		} else {
-			// if no roles for this client exist yet, then of course, all requested roles need to be created
-			clientRolesToAdd[clientId] = requestedClientRoles
-		}
-	}
-
-	// now check all existing roles, if there are even any roles configured for each client
-	for clientId, existingClientRoles := range existingRoles.clientRoles {
-		if _, ok := requestedRoles.clientRoles[clientId]; !ok {
-			// no role requested for this client? -> remove all existing client-roles
-			clientRolesToRemove[clientId] = existingClientRoles
-		}
-	}
-
-	updates := terraformRoleMappingUpdates{
-		realmRolesToRemove:  realmRolesToRemove,
-		realmRolesToAdd:     realmRolesToAdd,
-		clientRolesToRemove: clientRolesToRemove,
-		clientRolesToAdd:    clientRolesToAdd,
-	}
-
-	return &updates
-}
-
-// check if given role exists in a list of roles
-func roleExists(roles []*keycloak.Role, role *keycloak.Role) bool {
-	for _, r := range roles {
-		if r.Id == role.Id {
-			return true
-		}
-	}
-
-	return false
-}
-
-// calculate the set difference: returns `a \ b`, i.e. every role that exist in a, but not in b
-func minusRoles(a, b []*keycloak.Role) []*keycloak.Role {
-	var aWithoutB []*keycloak.Role
-
-	for _, role := range a {
-		if !roleExists(b, role) {
-			aWithoutB = append(aWithoutB, role)
-		}
-	}
-
-	return aWithoutB
-}
diff --git a/provider/role_mapping_helpers.go b/provider/role_mapping_helpers.go
new file mode 100644
index 0000000..1429b21
--- /dev/null
+++ b/provider/role_mapping_helpers.go
@@ -0,0 +1,121 @@
+package provider
+
+import "github.com/mrparkers/terraform-provider-keycloak/keycloak"
+
+// a struct that represents the "desired" state configured via terraform
+// the key for 'clientRoles' is keycloak's client-id (the uuid, not to be confused with the OAuth Client Id)
+type roleMapping struct {
+	clientRoles map[string][]*keycloak.Role
+	realmRoles  []*keycloak.Role
+}
+
+// transform the keycloak response RoleMapping into the internal roleMapping
+// so we can compare the current state (keycloak) against the desired state (terraform)
+func intoRoleMapping(keycloakRoleMapping *keycloak.RoleMapping) *roleMapping {
+	clientRoles := make(map[string][]*keycloak.Role)
+	for _, clientRoleMapping := range keycloakRoleMapping.ClientMappings {
+		clientRoles[clientRoleMapping.Id] = clientRoleMapping.Mappings
+	}
+
+	mapping := roleMapping{
+		clientRoles: clientRoles,
+		realmRoles:  keycloakRoleMapping.RealmMappings,
+	}
+
+	return &mapping
+}
+
+// given a list of roleIds, query keycloak for role details to find out if a role is a client role or a
+// realm role (which is required to POST the role assignment to the correct API endpoint)
+func getExtendedRoleMapping(keycloakClient *keycloak.KeycloakClient, realmId string, roleIds []string) (*roleMapping, error) {
+	clientRoles := make(map[string][]*keycloak.Role)
+	var realmRoles []*keycloak.Role
+
+	for _, roleId := range roleIds {
+		role, err := keycloakClient.GetRole(realmId, roleId)
+		if err != nil {
+			return nil, err
+		}
+
+		if role.ClientRole {
+			clientRoles[role.ClientId] = append(clientRoles[role.ClientId], role)
+		} else {
+			realmRoles = append(realmRoles, role)
+		}
+	}
+
+	mapping := roleMapping{
+		clientRoles: clientRoles,
+		realmRoles:  realmRoles,
+	}
+
+	return &mapping, nil
+}
+
+type terraformRoleMappingUpdates struct {
+	realmRolesToRemove  []*keycloak.Role
+	realmRolesToAdd     []*keycloak.Role
+	clientRolesToRemove map[string][]*keycloak.Role
+	clientRolesToAdd    map[string][]*keycloak.Role
+}
+
+// given the existing roles (queried from keycloak) and the requested roles (via tf)
+// calculate the required updates, i.e. roles to remove and roles to add
+func calculateRoleMappingUpdates(requestedRoles *roleMapping, existingRoles *roleMapping) *terraformRoleMappingUpdates {
+	clientRolesToRemove := make(map[string][]*keycloak.Role)
+	clientRolesToAdd := make(map[string][]*keycloak.Role)
+
+	realmRolesToRemove := minusRoles(existingRoles.realmRoles, requestedRoles.realmRoles)
+	realmRolesToAdd := minusRoles(requestedRoles.realmRoles, existingRoles.realmRoles)
+
+	for clientId, requestedClientRoles := range requestedRoles.clientRoles {
+		if existingClientRoles, ok := existingRoles.clientRoles[clientId]; ok {
+			clientRolesToAdd[clientId] = minusRoles(requestedClientRoles, existingClientRoles)
+			clientRolesToRemove[clientId] = minusRoles(existingClientRoles, requestedClientRoles)
+		} else {
+			// if no roles for this client exist yet, then of course, all requested roles need to be created
+			clientRolesToAdd[clientId] = requestedClientRoles
+		}
+	}
+
+	// now check all existing roles, if there are even any roles configured for each client
+	for clientId, existingClientRoles := range existingRoles.clientRoles {
+		if _, ok := requestedRoles.clientRoles[clientId]; !ok {
+			// no role requested for this client? -> remove all existing client-roles
+			clientRolesToRemove[clientId] = existingClientRoles
+		}
+	}
+
+	updates := terraformRoleMappingUpdates{
+		realmRolesToRemove:  realmRolesToRemove,
+		realmRolesToAdd:     realmRolesToAdd,
+		clientRolesToRemove: clientRolesToRemove,
+		clientRolesToAdd:    clientRolesToAdd,
+	}
+
+	return &updates
+}
+
+// check if given role exists in a list of roles
+func roleExists(roles []*keycloak.Role, role *keycloak.Role) bool {
+	for _, r := range roles {
+		if r.Id == role.Id {
+			return true
+		}
+	}
+
+	return false
+}
+
+// calculate the set difference: returns `a \ b`, i.e. every role that exist in a, but not in b
+func minusRoles(a, b []*keycloak.Role) []*keycloak.Role {
+	var aWithoutB []*keycloak.Role
+
+	for _, role := range a {
+		if !roleExists(b, role) {
+			aWithoutB = append(aWithoutB, role)
+		}
+	}
+
+	return aWithoutB
+}


Test patch:
diff --git a/provider/resource_keycloak_group_roles_test.go b/provider/resource_keycloak_group_roles_test.go
index 7f6bd10..8194ffa 100644
--- a/provider/resource_keycloak_group_roles_test.go
+++ b/provider/resource_keycloak_group_roles_test.go
@@ -136,27 +136,6 @@ func TestAccKeycloakGroupRoles_update(t *testing.T) {
 	})
 }
 
-func flattenGroupRoles(keycloakClient *keycloak.KeycloakClient, group *keycloak.Group) ([]string, error) {
-	var roles []string
-
-	for _, realmRole := range group.RealmRoles {
-		roles = append(roles, realmRole)
-	}
-
-	for clientId, clientRoles := range group.ClientRoles {
-		client, err := keycloakClient.GetGenericClientByClientId(group.RealmId, clientId)
-		if err != nil {
-			return nil, err
-		}
-
-		for _, clientRole := range clientRoles {
-			roles = append(roles, fmt.Sprintf("%s/%s", client.Id, clientRole))
-		}
-	}
-
-	return roles, nil
-}
-
 func testAccCheckKeycloakGroupHasRoles(resourceName string) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		keycloakClient := testAccProvider.Meta().(*keycloak.KeycloakClient)
@@ -188,7 +167,12 @@ func testAccCheckKeycloakGroupHasRoles(resourceName string) resource.TestCheckFu
 			return err
 		}
 
-		groupRoles, err := flattenGroupRoles(keycloakClient, group)
+		groupRoleMappings, err := keycloakClient.GetGroupRoleMappings(realm, groupId)
+		if err != nil {
+			return err
+		}
+
+		groupRoles, err := flattenRoleMapping(groupRoleMappings)
 		if err != nil {
 			return err
 		}
diff --git a/provider/resource_keycloak_user_roles_test.go b/provider/resource_keycloak_user_roles_test.go
index 9ae9b6f..5b200b1 100644
--- a/provider/resource_keycloak_user_roles_test.go
+++ b/provider/resource_keycloak_user_roles_test.go
@@ -136,14 +136,14 @@ func TestAccKeycloakUserRoles_update(t *testing.T) {
 	})
 }
 
-func flattenUserRoles(userRoleMapping *keycloak.UserRoleMapping) ([]string, error) {
+func flattenRoleMapping(roleMapping *keycloak.RoleMapping) ([]string, error) {
 	var roles []string
 
-	for _, realmRole := range userRoleMapping.RealmMappings {
+	for _, realmRole := range roleMapping.RealmMappings {
 		roles = append(roles, realmRole.Name)
 	}
 
-	for _, clientRoleMapping := range userRoleMapping.ClientMappings {
+	for _, clientRoleMapping := range roleMapping.ClientMappings {
 		for _, clientRole := range clientRoleMapping.Mappings {
 			roles = append(roles, fmt.Sprintf("%s/%s", clientRoleMapping.Id, clientRole.Name))
 		}
@@ -188,7 +188,7 @@ func testAccCheckKeycloakUserHasRoles(resourceName string) resource.TestCheckFun
 			return err
 		}
 
-		userRoles, err := flattenUserRoles(userRoleMappings)
+		userRoles, err := flattenRoleMapping(userRoleMappings)
 		if err != nil {
 			return err
 		}