Repository: epam/edp-keycloak-operator (go)

Issue:
Add the ability to add client roles to KeycloakRealmUser
**Is your feature request related to a problem? Please describe.**
I would like the ability to add client roles to keycloak users

**Describe the solution you'd like**
 This is supported by the keycloak client service account already. I would like to port that functionality to the keycloak user resource.

Pull request:
feat: Add the ability to add client roles to KeycloakRealmUser
# Pull Request Template

## Description
Add the ability to add client roles to KeycloakRealmUser

Fixes #135 

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [x] Enhancement (non-breaking change which improves an existing feature or documentation)
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)

## How Has This Been Tested?
- Integration tests
- Manually

## Checklist:
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Pull Request contains one commit. I squash my commits.

Patch:
diff --git a/api/v1/keycloakclient_types.go b/api/v1/keycloakclient_types.go
index 3d2f770..b6aa74f 100644
--- a/api/v1/keycloakclient_types.go
+++ b/api/v1/keycloakclient_types.go
@@ -219,7 +219,7 @@ type ClientRole struct {
 	// ClientID is a client ID.
 	ClientID string `json:"clientId"`
 
-	// Roles is a list of client roles names assigned to service account.
+	// Roles is a list of client roles names assigned to user.
 	// +nullable
 	// +optional
 	Roles []string `json:"roles,omitempty"`
diff --git a/api/v1/keycloakrealmuser_types.go b/api/v1/keycloakrealmuser_types.go
index f9ce7d5..6def78b 100644
--- a/api/v1/keycloakrealmuser_types.go
+++ b/api/v1/keycloakrealmuser_types.go
@@ -45,6 +45,11 @@ type KeycloakRealmUserSpec struct {
 	// +optional
 	Roles []string `json:"roles,omitempty"`
 
+	// ClientRoles is a list of client roles assigned to user.
+	// +nullable
+	// +optional
+	ClientRoles []ClientRole `json:"clientRoles,omitempty"`
+
 	// Groups is a list of groups assigned to user.
 	// +nullable
 	// +optional
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index a613d70..3a2e49e 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -1514,6 +1514,13 @@ func (in *KeycloakRealmUserSpec) DeepCopyInto(out *KeycloakRealmUserSpec) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
+	if in.ClientRoles != nil {
+		in, out := &in.ClientRoles, &out.ClientRoles
+		*out = make([]ClientRole, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 	if in.Groups != nil {
 		in, out := &in.Groups, &out.Groups
 		*out = make([]string, len(*in))
diff --git a/config/crd/bases/v1.edp.epam.com_keycloakclients.yaml b/config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
index ac0c34c..c5efbbc 100644
--- a/config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
+++ b/config/crd/bases/v1.edp.epam.com_keycloakclients.yaml
@@ -600,7 +600,7 @@ spec:
                           type: string
                         roles:
                           description: Roles is a list of client roles names assigned
-                            to service account.
+                            to user.
                           items:
                             type: string
                           nullable: true
diff --git a/config/crd/bases/v1.edp.epam.com_keycloakrealmgroups.yaml b/config/crd/bases/v1.edp.epam.com_keycloakrealmgroups.yaml
index 4610d85..d7f7f27 100644
--- a/config/crd/bases/v1.edp.epam.com_keycloakrealmgroups.yaml
+++ b/config/crd/bases/v1.edp.epam.com_keycloakrealmgroups.yaml
@@ -67,7 +67,7 @@ spec:
                       type: string
                     roles:
                       description: Roles is a list of client roles names assigned
-                        to service account.
+                        to user.
                       items:
                         type: string
                       nullable: true
diff --git a/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml b/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
index 6e9ef60..0fb5480 100644
--- a/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
+++ b/config/crd/bases/v1.edp.epam.com_keycloakrealmusers.yaml
@@ -50,6 +50,25 @@ spec:
                 description: Attributes is a map of user attributes.
                 nullable: true
                 type: object
+              clientRoles:
+                description: ClientRoles is a list of client roles assigned to user.
+                items:
+                  properties:
+                    clientId:
+                      description: ClientID is a client ID.
+                      type: string
+                    roles:
+                      description: Roles is a list of client roles names assigned
+                        to user.
+                      items:
+                        type: string
+                      nullable: true
+                      type: array
+                  required:
+                  - clientId
+                  type: object
+                nullable: true
+                type: array
               email:
                 description: Email is a user email.
                 type: string
diff --git a/deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml b/deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
index ac0c34c..c5efbbc 100644
--- a/deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
+++ b/deploy-templates/crds/v1.edp.epam.com_keycloakclients.yaml
@@ -600,7 +600,7 @@ spec:
                           type: string
                         roles:
                           description: Roles is a list of client roles names assigned
-                            to service account.
+                            to user.
                           items:
                             type: string
                           nullable: true
diff --git a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmgroups.yaml b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmgroups.yaml
index 4610d85..d7f7f27 100644
--- a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmgroups.yaml
+++ b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmgroups.yaml
@@ -67,7 +67,7 @@ spec:
                       type: string
                     roles:
                       description: Roles is a list of client roles names assigned
-                        to service account.
+                        to user.
                       items:
                         type: string
                       nullable: true
diff --git a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
index 6e9ef60..0fb5480 100644
--- a/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
+++ b/deploy-templates/crds/v1.edp.epam.com_keycloakrealmusers.yaml
@@ -50,6 +50,25 @@ spec:
                 description: Attributes is a map of user attributes.
                 nullable: true
                 type: object
+              clientRoles:
+                description: ClientRoles is a list of client roles assigned to user.
+                items:
+                  properties:
+                    clientId:
+                      description: ClientID is a client ID.
+                      type: string
+                    roles:
+                      description: Roles is a list of client roles names assigned
+                        to user.
+                      items:
+                        type: string
+                      nullable: true
+                      type: array
+                  required:
+                  - clientId
+                  type: object
+                nullable: true
+                type: array
               email:
                 description: Email is a user email.
                 type: string
diff --git a/docs/api.md b/docs/api.md
index a991d3b..5559913 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -3153,7 +3153,7 @@ ServiceAccount is a service account configuration.
         <td><b>roles</b></td>
         <td>[]string</td>
         <td>
-          Roles is a list of client roles names assigned to service account.<br/>
+          Roles is a list of client roles names assigned to user.<br/>
         </td>
         <td>false</td>
       </tr></tbody>
@@ -3876,7 +3876,7 @@ RealmRef is reference to Realm custom resource.
         <td><b>roles</b></td>
         <td>[]string</td>
         <td>
-          Roles is a list of client roles names assigned to service account.<br/>
+          Roles is a list of client roles names assigned to user.<br/>
         </td>
         <td>false</td>
       </tr></tbody>
@@ -6108,6 +6108,13 @@ KeycloakRealmUserSpec defines the desired state of KeycloakRealmUser.
           Attributes is a map of user attributes.<br/>
         </td>
         <td>false</td>
+      </tr><tr>
+        <td><b><a href="#keycloakrealmuserspecclientrolesindex">clientRoles</a></b></td>
+        <td>[]object</td>
+        <td>
+          ClientRoles is a list of client roles assigned to user.<br/>
+        </td>
+        <td>false</td>
       </tr><tr>
         <td><b>email</b></td>
         <td>string</td>
@@ -6247,6 +6254,40 @@ RealmRef is reference to Realm custom resource.
 </table>
 
 
+### KeycloakRealmUser.spec.clientRoles[index]
+<sup><sup>[↩ Parent](#keycloakrealmuserspec)</sup></sup>
+
+
+
+
+
+<table>
+    <thead>
+        <tr>
+            <th>Name</th>
+            <th>Type</th>
+            <th>Description</th>
+            <th>Required</th>
+        </tr>
+    </thead>
+    <tbody><tr>
+        <td><b>clientId</b></td>
+        <td>string</td>
+        <td>
+          ClientID is a client ID.<br/>
+        </td>
+        <td>true</td>
+      </tr><tr>
+        <td><b>roles</b></td>
+        <td>[]string</td>
+        <td>
+          Roles is a list of client roles names assigned to user.<br/>
+        </td>
+        <td>false</td>
+      </tr></tbody>
+</table>
+
+
 ### KeycloakRealmUser.spec.passwordSecret
 <sup><sup>[↩ Parent](#keycloakrealmuserspec)</sup></sup>
 
diff --git a/internal/controller/keycloakrealmidentityprovider/keycloakrealmidentityprovider_controller.go b/internal/controller/keycloakrealmidentityprovider/keycloakrealmidentityprovider_controller.go
index a84fa4b..e70cf42 100644
--- a/internal/controller/keycloakrealmidentityprovider/keycloakrealmidentityprovider_controller.go
+++ b/internal/controller/keycloakrealmidentityprovider/keycloakrealmidentityprovider_controller.go
@@ -163,6 +163,7 @@ func (r *Reconcile) tryReconcile(ctx context.Context, keycloakRealmIDP *keycloak
 		kClient,
 		objectmeta.PreserveResourcesOnDeletion(keycloakRealmIDP),
 	)
+
 	deleted, err := r.helper.TryToDelete(ctx, keycloakRealmIDP, term, finalizerName)
 	if err != nil {
 		return fmt.Errorf("unable to delete realm idp: %w", err)
diff --git a/internal/controller/keycloakrealmuser/keycloakrealmuser_controller.go b/internal/controller/keycloakrealmuser/keycloakrealmuser_controller.go
index d2889da..7176dec 100644
--- a/internal/controller/keycloakrealmuser/keycloakrealmuser_controller.go
+++ b/internal/controller/keycloakrealmuser/keycloakrealmuser_controller.go
@@ -110,6 +110,19 @@ func (r *Reconcile) Reconcile(ctx context.Context, request reconcile.Request) (c
 	return ctrl.Result{}, nil
 }
 
+func convertClientRoles(apiClientRoles []keycloakApi.ClientRole) map[string][]string {
+	if apiClientRoles == nil {
+		return nil
+	}
+
+	clientRolesMap := make(map[string][]string)
+	for _, apiRole := range apiClientRoles {
+		clientRolesMap[apiRole.ClientID] = apiRole.Roles
+	}
+
+	return clientRolesMap
+}
+
 func (r *Reconcile) tryReconcile(ctx context.Context, instance *keycloakApi.KeycloakRealmUser) error {
 	err := r.helper.SetRealmOwnerRef(ctx, instance)
 	if err != nil {
@@ -165,6 +178,7 @@ func (r *Reconcile) tryReconcile(ctx context.Context, instance *keycloakApi.Keyc
 		Username:            userSpec.Username,
 		Groups:              userSpec.Groups,
 		Roles:               userSpec.Roles,
+		ClientRoles:         convertClientRoles(userSpec.ClientRoles),
 		RequiredUserActions: userSpec.RequiredUserActions,
 		LastName:            userSpec.LastName,
 		FirstName:           userSpec.FirstName,
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_service_account.go b/pkg/client/keycloak/adapter/gocloak_adapter_service_account.go
index 3faf385..795717f 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_service_account.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_service_account.go
@@ -14,32 +14,7 @@ func (a GoCloakAdapter) SyncServiceAccountRoles(realm, clientID string, realmRol
 		return errors.Wrap(err, "unable to get client service account")
 	}
 
-	roleMappings, err := a.client.GetRoleMappingByUserID(context.Background(), a.token.AccessToken, realm, *user.ID)
-	if err != nil {
-		return errors.Wrap(err, "error during GetRoleMappingByUserID")
-	}
-
-	deleteRealmRoleFunc := a.client.DeleteRealmRoleFromUser
-	if addOnly {
-		deleteRealmRoleFunc = doNotDeleteRealmRoleFromUser
-	}
-
-	if err := a.syncEntityRealmRoles(*user.ID, realm, realmRoles, roleMappings.RealmMappings,
-		a.client.AddRealmRoleToUser, deleteRealmRoleFunc); err != nil {
-		return errors.Wrap(err, "unable to sync service account realm roles")
-	}
-
-	deleteClientRoleFromUserFunc := a.client.DeleteClientRoleFromUser
-	if addOnly {
-		deleteClientRoleFromUserFunc = doNotDeleteClientRoleFromUser
-	}
-
-	if err := a.syncEntityClientRoles(realm, *user.ID, clientRoles, roleMappings.ClientMappings,
-		a.client.AddClientRoleToUser, deleteClientRoleFromUserFunc); err != nil {
-		return errors.Wrap(err, "unable to sync service account client roles")
-	}
-
-	return nil
+	return a.SyncUserRoles(context.Background(), realm, *user.ID, realmRoles, clientRoles, addOnly)
 }
 
 func (a GoCloakAdapter) SyncServiceAccountGroups(realm, clientID string, groups []string, addOnly bool) error {
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_user.go b/pkg/client/keycloak/adapter/gocloak_adapter_user.go
index 556cefc..c1474fe 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_user.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_user.go
@@ -19,6 +19,7 @@ type KeycloakUser struct {
 	LastName            string
 	RequiredUserActions []string
 	Roles               []string
+	ClientRoles         map[string][]string
 	Groups              []string
 	Attributes          map[string]string
 	Password            string
@@ -47,7 +48,7 @@ func (a GoCloakAdapter) SyncRealmUser(ctx context.Context, realmName string, use
 		}
 	}
 
-	if err = a.syncUserRoles(ctx, realmName, userID, userDto.Roles, addOnly); err != nil {
+	if err = a.SyncUserRoles(ctx, realmName, userID, userDto.Roles, userDto.ClientRoles, addOnly); err != nil {
 		return err
 	}
 
@@ -180,39 +181,49 @@ func (a GoCloakAdapter) syncUserGroups(ctx context.Context, realmName string, us
 	return nil
 }
 
-func (a GoCloakAdapter) syncUserRoles(ctx context.Context, realmName string, userID string, roles []string, addOnly bool) error {
-	if !addOnly {
-		if err := a.clearUserRealmRoles(ctx, realmName, userID); err != nil {
-			return errors.Wrap(err, "unable to clear realm roles")
-		}
-	}
-
-	realmRoles, err := a.client.GetRealmRoles(ctx, a.token.AccessToken, realmName, gocloak.GetRoleParams{
-		Max:                 gocloak.IntP(100),
-		BriefRepresentation: gocloak.BoolP(true),
-	})
+// SyncUserRoles syncs user realm and client roles.
+func (a GoCloakAdapter) SyncUserRoles(
+	ctx context.Context,
+	realm, userID string,
+	realmRoles []string,
+	clientRoles map[string][]string,
+	addOnly bool,
+) error {
+	roleMappings, err := a.client.GetRoleMappingByUserID(ctx, a.token.AccessToken, realm, userID)
 	if err != nil {
-		return fmt.Errorf("unable to get realm roles: %w", err)
+		return fmt.Errorf("error during GetRoleMappingByUserID: %w", err)
 	}
 
-	realmRolesDict := make(map[string]gocloak.Role, len(realmRoles))
-	for _, role := range realmRoles {
-		realmRolesDict[*role.Name] = *role
+	deleteRealmRoleFunc := a.client.DeleteRealmRoleFromUser
+	if addOnly {
+		deleteRealmRoleFunc = doNotDeleteRealmRoleFromUser
 	}
 
-	kcRoles := make([]gocloak.Role, 0, len(roles))
-
-	for _, roleName := range roles {
-		role, ok := realmRolesDict[roleName]
-		if !ok {
-			return errors.Errorf("realm role %s not found", roleName)
-		}
+	if err := a.syncEntityRealmRoles(
+		userID,
+		realm,
+		realmRoles,
+		roleMappings.RealmMappings,
+		a.client.AddRealmRoleToUser,
+		deleteRealmRoleFunc,
+	); err != nil {
+		return fmt.Errorf("unable to sync user realm roles: %w", err)
+	}
 
-		kcRoles = append(kcRoles, role)
+	deleteClientRoleFromUserFunc := a.client.DeleteClientRoleFromUser
+	if addOnly {
+		deleteClientRoleFromUserFunc = doNotDeleteClientRoleFromUser
 	}
 
-	if err = a.client.AddRealmRoleToUser(ctx, a.token.AccessToken, realmName, userID, kcRoles); err != nil {
-		return fmt.Errorf("unable to add realm roles to user: %w", err)
+	if err := a.syncEntityClientRoles(
+		realm,
+		userID,
+		clientRoles,
+		roleMappings.ClientMappings,
+		a.client.AddClientRoleToUser,
+		deleteClientRoleFromUserFunc,
+	); err != nil {
+		return fmt.Errorf("unable to sync user client roles: %w", err)
 	}
 
 	return nil
@@ -231,7 +242,7 @@ func (a GoCloakAdapter) GetUserRealmRoleMappings(ctx context.Context, realmName
 		Get(a.buildPath(getUserRealmRoleMappings))
 
 	if err = a.checkError(err, rsp); err != nil {
-		return nil, errors.Wrap(err, "unable to get realm role mappings")
+		return nil, fmt.Errorf("unable to get realm role mappings: %w", err)
 	}
 
 	return roles, nil
@@ -250,7 +261,7 @@ func (a GoCloakAdapter) GetUserGroupMappings(ctx context.Context, realmName stri
 		Get(a.buildPath(getUserGroupMappings))
 
 	if err = a.checkError(err, rsp); err != nil {
-		return nil, errors.Wrap(err, "unable to get group mappings")
+		return nil, fmt.Errorf("unable to get group mappings: %w", err)
 	}
 
 	return groups, nil
@@ -267,7 +278,7 @@ func (a GoCloakAdapter) RemoveUserFromGroup(ctx context.Context, realmName, user
 		Delete(a.buildPath(manageUserGroups))
 
 	if err = a.checkError(err, rsp); err != nil {
-		return errors.Wrap(err, "unable to remove user from group")
+		return fmt.Errorf("unable to remove user from group: %w", err)
 	}
 
 	return nil
@@ -289,7 +300,7 @@ func (a GoCloakAdapter) AddUserToGroup(ctx context.Context, realmName, userID, g
 		Put(a.buildPath(manageUserGroups))
 
 	if err = a.checkError(err, rsp); err != nil {
-		return errors.Wrap(err, "unable to add user to group")
+		return fmt.Errorf("unable to add user to group: %w", err)
 	}
 
 	return nil
@@ -346,25 +357,7 @@ func checkHttpResp(res *keycloak_go_client.Response, err error) error {
 	const maxStatusCodesSuccess = 399
 
 	if res.HTTPResponse.StatusCode > maxStatusCodesSuccess {
-		return errors.Errorf("status: %s, body: %s", res.HTTPResponse.Status, res.Body)
-	}
-
-	return nil
-}
-
-func (a GoCloakAdapter) clearUserRealmRoles(ctx context.Context, realmName string, userID string) error {
-	roles, err := a.GetUserRealmRoleMappings(ctx, realmName, userID)
-	if err != nil {
-		return errors.Wrap(err, "unable to get user realm role map")
-	}
-
-	goRoles := make([]gocloak.Role, 0, len(roles))
-	for i := range roles {
-		goRoles = append(goRoles, gocloak.Role{ID: &roles[i].ID, Name: &roles[i].Name})
-	}
-
-	if err := a.client.DeleteRealmRoleFromUser(ctx, a.token.AccessToken, realmName, userID, goRoles); err != nil {
-		return errors.Wrap(err, "unable to delete realm role from user")
+		return fmt.Errorf("status: %s, body: %s", res.HTTPResponse.Status, res.Body)
 	}
 
 	return nil
@@ -384,7 +377,7 @@ func (a GoCloakAdapter) setUserPassword(realmName, userID, password string) erro
 		Put(a.buildPath(setRealmUserPassword))
 
 	if err = a.checkError(err, rsp); err != nil {
-		return errors.Wrap(err, "unable to set user password")
+		return fmt.Errorf("unable to set user password: %w", err)
 	}
 
 	return nil


Test patch:
diff --git a/internal/controller/keycloakrealmuser/keycloakrealmuser_controller_integration_test.go b/internal/controller/keycloakrealmuser/keycloakrealmuser_controller_integration_test.go
index 19e79b9..e60f854 100644
--- a/internal/controller/keycloakrealmuser/keycloakrealmuser_controller_integration_test.go
+++ b/internal/controller/keycloakrealmuser/keycloakrealmuser_controller_integration_test.go
@@ -93,7 +93,7 @@ var _ = Describe("KeycloakRealmUser controller", Ordered, func() {
 		}
 		Expect(k8sClient.Create(ctx, secret)).Should(Succeed())
 
-		By("Creating a KeycloakRealmUser")
+		By("Creating a KeycloakRealmUser with realm roles and client roles")
 		user := &keycloakApi.KeycloakRealmUser{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      userCR,
@@ -114,7 +114,23 @@ var _ = Describe("KeycloakRealmUser controller", Ordered, func() {
 					"UPDATE_PASSWORD",
 				},
 				Roles: []string{
-					"test-user-role",
+					"offline_access",
+					"uma_authorization",
+				},
+				ClientRoles: []keycloakApi.ClientRole{
+					{
+						ClientID: "account",
+						Roles: []string{
+							"view-profile",
+							"view-groups",
+						},
+					},
+					{
+						ClientID: "realm-management",
+						Roles: []string{
+							"create-client",
+						},
+					},
 				},
 				Groups: []string{
 					"test-user-group",
@@ -138,6 +154,63 @@ var _ = Describe("KeycloakRealmUser controller", Ordered, func() {
 			g.Expect(err).ShouldNot(HaveOccurred())
 			g.Expect(createdUser.Status.Value).Should(Equal(helper.StatusOK))
 		}).WithTimeout(time.Second * 20).WithPolling(time.Second).Should(Succeed())
+
+		// Verify that the roles were created in Keycloak
+		Eventually(func(g Gomega) {
+			users, err := keycloakApiClient.GetUsers(ctx, getKeyCloakToken(), KeycloakRealmCR, gocloak.GetUsersParams{
+				Username: gocloak.StringP(user.Spec.Username),
+				Exact:    gocloak.BoolP(true),
+			})
+			g.Expect(err).ShouldNot(HaveOccurred())
+			g.Expect(users).Should(HaveLen(1))
+
+			// Get user role mappings to verify realm roles
+			roleMappings, err := keycloakApiClient.GetRoleMappingByUserID(ctx, getKeyCloakToken(), KeycloakRealmCR, *users[0].ID)
+			g.Expect(err).ShouldNot(HaveOccurred())
+
+			// Check realm roles
+			realmRoleNames := make([]string, 0)
+			if roleMappings.RealmMappings != nil {
+				for _, role := range *roleMappings.RealmMappings {
+					if role.Name != nil {
+						realmRoleNames = append(realmRoleNames, *role.Name)
+					}
+				}
+			}
+			g.Expect(realmRoleNames).Should(ContainElement("offline_access"))
+			g.Expect(realmRoleNames).Should(ContainElement("uma_authorization"))
+
+			// Check client roles for account client
+			accountClientRoleNames := make([]string, 0)
+			for _, clientMapping := range roleMappings.ClientMappings {
+				if clientMapping.Client != nil && *clientMapping.Client == "account" {
+					if clientMapping.Mappings != nil {
+						for _, role := range *clientMapping.Mappings {
+							if role.Name != nil {
+								accountClientRoleNames = append(accountClientRoleNames, *role.Name)
+							}
+						}
+					}
+				}
+			}
+			g.Expect(accountClientRoleNames).Should(ContainElement("view-profile"))
+			g.Expect(accountClientRoleNames).Should(ContainElement("view-groups"))
+
+			// Check client roles for realm-management client
+			realmManagementClientRoleNames := make([]string, 0)
+			for _, clientMapping := range roleMappings.ClientMappings {
+				if clientMapping.Client != nil && *clientMapping.Client == "realm-management" {
+					if clientMapping.Mappings != nil {
+						for _, role := range *clientMapping.Mappings {
+							if role.Name != nil {
+								realmManagementClientRoleNames = append(realmManagementClientRoleNames, *role.Name)
+							}
+						}
+					}
+				}
+			}
+			g.Expect(realmManagementClientRoleNames).Should(ContainElement("create-client"))
+		}, time.Minute, time.Second*5).Should(Succeed())
 	})
 	It("Should update KeycloakRealmUser", func() {
 		By("Getting KeycloakRealmUser")
@@ -167,6 +240,74 @@ var _ = Describe("KeycloakRealmUser controller", Ordered, func() {
 			g.Expect(*users[0].LastName).Should(Equal("new-last-name"))
 		}, time.Minute, time.Second*5).Should(Succeed())
 	})
+	It("Should update KeycloakRealmUser roles", func() {
+		By("Getting KeycloakRealmUser")
+		user := &keycloakApi.KeycloakRealmUser{}
+		Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: ns, Name: userCR}, user)).Should(Succeed())
+
+		By("Updating KeycloakRealmUser roles")
+		user.Spec.Roles = []string{
+			"offline_access",
+		}
+		user.Spec.ClientRoles = []keycloakApi.ClientRole{
+			{
+				ClientID: "account",
+				Roles: []string{
+					"view-profile",
+				},
+			},
+		}
+
+		Expect(k8sClient.Update(ctx, user)).Should(Succeed())
+		Eventually(func(g Gomega) {
+			updatedUser := &keycloakApi.KeycloakRealmUser{}
+			err := k8sClient.Get(ctx, types.NamespacedName{Name: user.Name, Namespace: ns}, updatedUser)
+			g.Expect(err).ShouldNot(HaveOccurred())
+			g.Expect(updatedUser.Status.Value).Should(Equal(helper.StatusOK))
+		}, time.Minute, time.Second*5).Should(Succeed())
+
+		// Verify that the roles were updated in Keycloak
+		Eventually(func(g Gomega) {
+			users, err := keycloakApiClient.GetUsers(ctx, getKeyCloakToken(), KeycloakRealmCR, gocloak.GetUsersParams{
+				Username: gocloak.StringP(user.Spec.Username),
+				Exact:    gocloak.BoolP(true),
+			})
+			g.Expect(err).ShouldNot(HaveOccurred())
+			g.Expect(users).Should(HaveLen(1))
+
+			// Get user role mappings to verify realm roles
+			roleMappings, err := keycloakApiClient.GetRoleMappingByUserID(ctx, getKeyCloakToken(), KeycloakRealmCR, *users[0].ID)
+			g.Expect(err).ShouldNot(HaveOccurred())
+
+			// Check realm roles
+			realmRoleNames := make([]string, 0)
+			if roleMappings.RealmMappings != nil {
+				for _, role := range *roleMappings.RealmMappings {
+					if role.Name != nil {
+						realmRoleNames = append(realmRoleNames, *role.Name)
+					}
+				}
+			}
+			g.Expect(realmRoleNames).Should(ContainElement("offline_access"))
+			g.Expect(realmRoleNames).ShouldNot(ContainElement("uma_authorization"))
+
+			// Check client roles
+			clientRoleNames := make([]string, 0)
+			for _, clientMapping := range roleMappings.ClientMappings {
+				if clientMapping.Client != nil && *clientMapping.Client == "account" {
+					if clientMapping.Mappings != nil {
+						for _, role := range *clientMapping.Mappings {
+							if role.Name != nil {
+								clientRoleNames = append(clientRoleNames, *role.Name)
+							}
+						}
+					}
+				}
+			}
+			g.Expect(clientRoleNames).Should(ContainElement("view-profile"))
+			g.Expect(clientRoleNames).ShouldNot(ContainElement("view-groups"))
+		}, time.Minute, time.Second*5).Should(Succeed())
+	})
 	It("Should delete KeycloakRealmUser", func() {
 		By("Getting KeycloakRealmUser")
 		user := &keycloakApi.KeycloakRealmUser{}
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_service_account_test.go b/pkg/client/keycloak/adapter/gocloak_adapter_service_account_test.go
index 7a0b872..bd8c0bc 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_service_account_test.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_service_account_test.go
@@ -4,6 +4,8 @@ import (
 	"testing"
 
 	"github.com/Nerzal/gocloak/v12"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 
@@ -41,3 +43,355 @@ func TestGoCloakAdapter_SetServiceAccountAttributes(t *testing.T) {
 		map[string]string{"foo": "bar"}, true)
 	require.NoError(t, err)
 }
+
+func TestGoCloakAdapter_SyncServiceAccountRoles(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name        string
+		realm       string
+		clientID    string
+		realmRoles  []string
+		clientRoles map[string][]string
+		addOnly     bool
+		client      func(t *testing.T) *mocks.MockGoCloak
+		wantErr     require.ErrorAssertionFunc
+	}{
+		{
+			name:        "sync service account roles success",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1", "role2"},
+			clientRoles: map[string][]string{"client1": {"client-role1"}},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("role1"),
+						ID:   gocloak.StringP("role1-id"),
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role2").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("role2"),
+						ID:   gocloak.StringP("role2-id"),
+					}, nil)
+				m.On("AddRealmRoleToUser",
+					mock.Anything,
+					"",
+					"realm",
+					"service-account-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 2) &&
+							assert.Equal(t, "role1-id", *roles[0].ID) &&
+							assert.Equal(t, "role2-id", *roles[1].ID)
+					})).
+					Return(nil)
+				m.On("GetClients", mock.Anything, "", "realm", mock.Anything).
+					Return([]*gocloak.Client{{
+						ID:       gocloak.StringP("client1-id"),
+						ClientID: gocloak.StringP("client1"),
+					}}, nil)
+				m.On("GetClientRole", mock.Anything, "", "realm", "client1-id", "client-role1").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("client-role1"),
+						ID:   gocloak.StringP("client-role1-id"),
+					}, nil)
+				m.On("AddClientRoleToUser",
+					mock.Anything,
+					"",
+					"realm",
+					"client1-id",
+					"service-account-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 1) &&
+							assert.Equal(t, "client-role1-id", *roles[0].ID)
+					})).
+					Return(nil)
+
+				return m
+			},
+			wantErr: require.NoError,
+		},
+		{
+			name:        "sync service account roles add only",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1"},
+			clientRoles: map[string][]string{},
+			addOnly:     true,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("role1"),
+						ID:   gocloak.StringP("role1-id"),
+					}, nil)
+				m.On("AddRealmRoleToUser",
+					mock.Anything,
+					"",
+					"realm",
+					"service-account-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 1) &&
+							assert.Equal(t, "role1-id", *roles[0].ID)
+					})).
+					Return(nil)
+
+				return m
+			},
+			wantErr: require.NoError,
+		},
+		{
+			name:        "failed to get service account",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1"},
+			clientRoles: map[string][]string{},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(nil, errors.New("failed to get service account"))
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "failed to get service account")
+			},
+		},
+		{
+			name:        "failed to get role mapping",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1"},
+			clientRoles: map[string][]string{},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(nil, errors.New("failed to get role mapping"))
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "failed to get role mapping")
+			},
+		},
+		{
+			name:        "failed to get realm role",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1"},
+			clientRoles: map[string][]string{},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(nil, errors.New("failed to get realm role"))
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "failed to get realm role")
+			},
+		},
+		{
+			name:        "failed to get client ID",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{},
+			clientRoles: map[string][]string{"client1": {"client-role1"}},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetClients", mock.Anything, "", "realm", mock.Anything).
+					Return(nil, errors.New("failed to get clients"))
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "failed to get clients")
+			},
+		},
+		{
+			name:        "failed to get client role",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{},
+			clientRoles: map[string][]string{"client1": {"client-role1"}},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetClients", mock.Anything, "", "realm", mock.Anything).
+					Return([]*gocloak.Client{{
+						ID:       gocloak.StringP("client1-id"),
+						ClientID: gocloak.StringP("client1"),
+					}}, nil)
+				m.On("GetClientRole", mock.Anything, "", "realm", "client1-id", "client-role1").
+					Return(nil, errors.New("unable to get client role"))
+
+				return m
+			},
+			wantErr: func(t require.TestingT, err error, i ...interface{}) {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "unable to get client role")
+			},
+		},
+		{
+			name:        "sync with existing roles",
+			realm:       "realm",
+			clientID:    "client-id",
+			realmRoles:  []string{"role1"},
+			clientRoles: map[string][]string{},
+			addOnly:     false,
+			client: func(t *testing.T) *mocks.MockGoCloak {
+				m := mocks.NewMockGoCloak(t)
+
+				serviceAccountUser := &gocloak.User{
+					ID:       gocloak.StringP("service-account-id"),
+					Username: gocloak.StringP("service-account-user"),
+				}
+
+				existingRealmRoles := []gocloak.Role{
+					{
+						Name: gocloak.StringP("existing-role"),
+						ID:   gocloak.StringP("existing-role-id"),
+					},
+				}
+
+				m.On("GetClientServiceAccount", mock.Anything, "", "realm", "client-id").
+					Return(serviceAccountUser, nil)
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "service-account-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &existingRealmRoles,
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
+						Name: gocloak.StringP("role1"),
+						ID:   gocloak.StringP("role1-id"),
+					}, nil)
+				m.On("AddRealmRoleToUser",
+					mock.Anything,
+					"",
+					"realm",
+					"service-account-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 1) &&
+							assert.Equal(t, "role1-id", *roles[0].ID)
+					})).
+					Return(nil)
+				m.On("DeleteRealmRoleFromUser",
+					mock.Anything,
+					"",
+					"realm",
+					"service-account-id",
+					mock.MatchedBy(func(roles []gocloak.Role) bool {
+						return assert.Len(t, roles, 1) &&
+							assert.Equal(t, "existing-role-id", *roles[0].ID)
+					})).
+					Return(nil)
+
+				return m
+			},
+			wantErr: require.NoError,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := GoCloakAdapter{
+				client:   tt.client(t),
+				basePath: "",
+				token:    &gocloak.JWT{AccessToken: ""},
+			}
+
+			tt.wantErr(t, a.SyncServiceAccountRoles(
+				tt.realm,
+				tt.clientID,
+				tt.realmRoles,
+				tt.clientRoles,
+				tt.addOnly,
+			))
+		})
+	}
+}
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_test.go b/pkg/client/keycloak/adapter/gocloak_adapter_test.go
index fac80c1..e214449 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_test.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_test.go
@@ -508,107 +508,6 @@ func TestGoCloakAdapter_SyncClientProtocolMapper_ClientIDFailure(t *testing.T) {
 	assert.ErrorIs(t, err, mockErr)
 }
 
-func TestGoCloakAdapter_SyncServiceAccountRoles_AddOnly(t *testing.T) {
-	mockClient := mocks.NewMockGoCloak(t)
-	adapter := GoCloakAdapter{
-		client:   mockClient,
-		token:    &gocloak.JWT{AccessToken: "token"},
-		basePath: "",
-		log:      mock.NewLogr(),
-	}
-
-	mockClient.On("GetClientServiceAccount", testifymock.Anything, "token", "realm", "client").Return(&gocloak.User{
-		ID: gocloak.StringP("id"),
-	}, nil)
-
-	mockClient.On("GetRoleMappingByUserID", testifymock.Anything, "token", "realm", "id").
-		Return(&gocloak.MappingsRepresentation{}, nil)
-	mockClient.On("GetRealmRole", testifymock.Anything, "token", "realm", "foo").
-		Return(&gocloak.Role{}, nil)
-	mockClient.On("AddRealmRoleToUser", testifymock.Anything, "token", "realm", "id", []gocloak.Role{{}}).
-		Return(nil)
-	mockClient.On("GetClients", testifymock.Anything, "token", "realm",
-		gocloak.GetClientsParams{ClientID: gocloak.StringP("bar")}).Return(nil,
-		errors.New("get clients fatal"))
-
-	err := adapter.SyncServiceAccountRoles("realm", "client", []string{"foo"},
-		map[string][]string{
-			"bar": {"john"},
-		}, true)
-	require.Error(t, err)
-
-	if !strings.Contains(err.Error(),
-		"unable to sync service account client roles: error during syncOneEntityClientRole: unable to get client id, realm: realm, clientID bar: unable to get realm clients: get clients fatal") {
-		t.Fatalf("wrong error returned: %s", err.Error())
-	}
-}
-
-func TestGoCloakAdapter_SyncServiceAccountRoles(t *testing.T) {
-	mockClient := mocks.NewMockGoCloak(t)
-	adapter := GoCloakAdapter{
-		client:   mockClient,
-		token:    &gocloak.JWT{AccessToken: "token"},
-		basePath: "",
-		log:      mock.NewLogr(),
-	}
-
-	mockClient.On("GetClientServiceAccount", testifymock.Anything, "token", "realm", "client").Return(&gocloak.User{
-		ID: gocloak.StringP("id"),
-	}, nil)
-	mockClient.On("GetRoleMappingByUserID", testifymock.Anything, "token", "realm", "id").
-		Return(&gocloak.MappingsRepresentation{RealmMappings: &[]gocloak.Role{
-			{Name: gocloak.StringP("exist_realm_role1")},
-			{Name: gocloak.StringP("exist_realm_role2")},
-		}, ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{
-			"zabrod": {Client: gocloak.StringP("zabrod"), ID: gocloak.StringP("iiss123"),
-				Mappings: &[]gocloak.Role{
-					{Name: gocloak.StringP("exist_client_role1")},
-					{Name: gocloak.StringP("exist_client_role2")},
-				}},
-			"foo": {Client: gocloak.StringP("foo"), ID: gocloak.StringP("foo321"),
-				Mappings: &[]gocloak.Role{
-					{Name: gocloak.StringP("baz")},
-					{Name: gocloak.StringP("zaz")},
-				}},
-		}}, nil)
-	mockClient.On("GetRealmRole", testifymock.Anything, "token", "realm", "foo").
-		Return(&gocloak.Role{}, nil)
-	mockClient.On("GetRealmRole", testifymock.Anything, "token", "realm", "bar").
-		Return(&gocloak.Role{}, nil)
-	mockClient.On("AddRealmRoleToUser", testifymock.Anything, "token", "realm", "id", []gocloak.Role{{}, {}}).
-		Return(nil)
-	mockClient.On("GetClients", testifymock.Anything, "token", "realm",
-		gocloak.GetClientsParams{ClientID: gocloak.StringP("foo")}).Return([]*gocloak.Client{
-		{ClientID: gocloak.StringP("foo"), ID: gocloak.StringP("foo321")},
-	}, nil)
-	mockClient.On("GetClients", testifymock.Anything, "token", "realm",
-		gocloak.GetClientsParams{ClientID: gocloak.StringP("bar")}).Return([]*gocloak.Client{
-		{ClientID: gocloak.StringP("bar"), ID: gocloak.StringP("bar321")},
-	}, nil)
-	mockClient.On("GetClientRole", testifymock.Anything, "token", "realm", "foo321", "foo").Return(&gocloak.Role{}, nil)
-	mockClient.On("GetClientRole", testifymock.Anything, "token", "realm", "foo321", "bar").Return(&gocloak.Role{}, nil)
-	mockClient.On("GetClientRole", testifymock.Anything, "token", "realm", "bar321", "john").Return(&gocloak.Role{}, nil)
-	mockClient.On("AddClientRoleToUser", testifymock.Anything, "token", "realm", "foo321", "id", testifymock.Anything).
-		Return(nil)
-	mockClient.On("AddClientRoleToUser", testifymock.Anything, "token", "realm", "bar321", "id", testifymock.Anything).
-		Return(nil)
-	mockClient.On("DeleteRealmRoleFromUser", testifymock.Anything, "token", "realm", "id", testifymock.Anything).Return(nil)
-	mockClient.On("DeleteClientRoleFromUser", testifymock.Anything, "token", "realm", "foo321", "id",
-		testifymock.MatchedBy(func(roles []gocloak.Role) bool {
-			return len(roles) == 2
-		}),
-	).Return(nil)
-	mockClient.On("DeleteClientRoleFromUser", testifymock.Anything, "token", "realm", "iiss123", "id", testifymock.Anything).Return(nil)
-
-	if err := adapter.SyncServiceAccountRoles("realm", "client", []string{"foo", "bar"},
-		map[string][]string{
-			"foo": {"foo", "bar"},
-			"bar": {"john"},
-		}, false); err != nil {
-		require.NoError(t, err)
-	}
-}
-
 func TestGoCloakAdapter_DeleteGroup(t *testing.T) {
 	mockClient := mocks.NewMockGoCloak(t)
 	adapter := GoCloakAdapter{
diff --git a/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go b/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
index 4f170b9..00b29ea 100644
--- a/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
+++ b/pkg/client/keycloak/adapter/gocloak_adapter_user_test.go
@@ -74,11 +74,16 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						return assert.Equal(t, "user", *user.Username)
 					})).
 					Return("user-id", nil)
-				m.On("GetRealmRoles", mock.Anything, "", "realm", mock.Anything).
-					Return([]*gocloak.Role{{
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "user-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
 						Name: gocloak.StringP("role1"),
 						ID:   gocloak.StringP("role1-id"),
-					}}, nil)
+					}, nil)
 				m.On("AddRealmRoleToUser",
 					mock.Anything,
 					"",
@@ -99,13 +104,6 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						ID:   gocloak.StringP("group1-id"),
 					}}, nil)
 				m.On("RestyClient").Return(resty.New())
-				m.On("DeleteRealmRoleFromUser",
-					mock.Anything,
-					"",
-					"realm",
-					"user-id",
-					mock.Anything,
-				).Return(nil)
 				m.On("GetUserFederatedIdentities",
 					mock.Anything,
 					"",
@@ -164,11 +162,16 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						return assert.Equal(t, "user", *user.Username)
 					})).
 					Return(nil)
-				m.On("GetRealmRoles", mock.Anything, "", "realm", mock.Anything).
-					Return([]*gocloak.Role{{
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "user-with-groups-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
 						Name: gocloak.StringP("role1"),
 						ID:   gocloak.StringP("role1-id"),
-					}}, nil)
+					}, nil)
 				m.On("AddRealmRoleToUser",
 					mock.Anything,
 					"",
@@ -199,13 +202,6 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						},
 					}, nil)
 				m.On("RestyClient").Return(resty.New())
-				m.On("DeleteRealmRoleFromUser",
-					mock.Anything,
-					"",
-					"realm",
-					"user-with-groups-id",
-					mock.Anything,
-				).Return(nil)
 
 				return m
 			},
@@ -239,11 +235,16 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						return assert.Equal(t, "user", *user.Username)
 					})).
 					Return("user-id", nil)
-				m.On("GetRealmRoles", mock.Anything, "", "realm", mock.Anything).
-					Return([]*gocloak.Role{{
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "user-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
+					Return(&gocloak.Role{
 						Name: gocloak.StringP("role1"),
 						ID:   gocloak.StringP("role1-id"),
-					}}, nil)
+					}, nil)
 				m.On("AddRealmRoleToUser",
 					mock.Anything,
 					"",
@@ -261,13 +262,6 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 					mock.Anything).
 					Return(nil, errors.New("failed to get groups"))
 				m.On("RestyClient").Return(resty.New())
-				m.On("DeleteRealmRoleFromUser",
-					mock.Anything,
-					"",
-					"realm",
-					"user-id",
-					mock.Anything,
-				).Return(nil)
 
 				return m
 			},
@@ -307,16 +301,14 @@ func TestGoCloakAdapter_SyncRealmUser(t *testing.T) {
 						return assert.Equal(t, "user", *user.Username)
 					})).
 					Return(nil)
-				m.On("GetRealmRoles", mock.Anything, "", "realm", mock.Anything).
+				m.On("GetRoleMappingByUserID", mock.Anything, "", "realm", "user-id").
+					Return(&gocloak.MappingsRepresentation{
+						RealmMappings:  &[]gocloak.Role{},
+						ClientMappings: map[string]*gocloak.ClientMappingsRepresentation{},
+					}, nil)
+				m.On("GetRealmRole", mock.Anything, "", "realm", "role1").
 					Return(nil, errors.New("failed to get roles"))
 				m.On("RestyClient").Return(resty.New())
-				m.On("DeleteRealmRoleFromUser",
-					mock.Anything,
-					"",
-					"realm",
-					"user-id",
-					mock.Anything,
-				).Return(nil)
 
 				return m
 			},