diff --git a/pom.xml b/pom.xml
index 64f9471..8e34afd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -186,6 +186,13 @@
+
+ org.assertj
+ assertj-core
+ 3.20.2
+ test
+
+
org.mockito
mockito-core
diff --git a/src/main/java/com/kiwigrid/keycloak/controller/client/ClientController.java b/src/main/java/com/kiwigrid/keycloak/controller/client/ClientController.java
index d70f842..4fa3f49 100644
--- a/src/main/java/com/kiwigrid/keycloak/controller/client/ClientController.java
+++ b/src/main/java/com/kiwigrid/keycloak/controller/client/ClientController.java
@@ -2,7 +2,6 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
@@ -19,8 +18,6 @@
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.keycloak.admin.client.resource.RealmResource;
-import org.keycloak.admin.client.resource.RoleMappingResource;
-import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
@@ -30,15 +27,18 @@ public class ClientController extends KubernetesController {
final KeycloakController keycloak;
final AssignedClientScopesSyncer assignedClientScopesSyncer;
+ final ServiceAccountRoleAssignmentSynchronizer serviceAccountRoleAssignmentSynchronizer;
public ClientController(KeycloakController keycloak,
KubernetesClient kubernetes,
- AssignedClientScopesSyncer assignedClientScopesSyncer) {
+ AssignedClientScopesSyncer assignedClientScopesSyncer,
+ ServiceAccountRoleAssignmentSynchronizer serviceAccountRoleAssignmentSynchronizer) {
super(kubernetes, ClientResource.DEFINITION, ClientResource.class, ClientResource.ClientResourceList.class,
ClientResource.ClientResourceDoneable.class);
this.keycloak = keycloak;
this.assignedClientScopesSyncer = assignedClientScopesSyncer;
+ this.serviceAccountRoleAssignmentSynchronizer = serviceAccountRoleAssignmentSynchronizer;
}
@Override
@@ -94,6 +94,7 @@ public void apply(ClientResource clientResource) {
if (clientResource.getSpec().getServiceAccountsEnabled() == Boolean.TRUE) {
manageServiceAccountRealmRoles(realmResource, clientUuid, clientResource);
+
}
updateStatus(clientResource, null);
} catch (RuntimeException e) {
@@ -364,57 +365,9 @@ void manageMapper(RealmResource realmResource, String clientUuid, ClientResource
}
private void manageServiceAccountRealmRoles(RealmResource realmResource, String clientUuid, ClientResource clientResource) {
- var keycloak = clientResource.getSpec().getKeycloak();
- var realm = clientResource.getSpec().getRealm();
- var clientId = clientResource.getSpec().getClientId();
-
- org.keycloak.admin.client.resource.ClientResource keycloakClientResource = realmResource.clients().get(clientUuid);
- RoleMappingResource serviceAccountRolesMapping = realmResource.users()
- .get(keycloakClientResource.getServiceAccountUser().getId())
- .roles();
-
- List requestedRealmRoles = clientResource.getSpec().getServiceAccountRealmRoles();
-
- removeRoleMappingNotRequestedAnymore(keycloak, realm, clientId, serviceAccountRolesMapping, requestedRealmRoles);
-
- List realmRoleNames = realmResource.roles().list().stream().map(RoleRepresentation::getName).collect(Collectors.toList());
- List rolesToCreate = requestedRealmRoles.stream().filter(role -> !realmRoleNames.contains(role)).collect(Collectors.toList());
- createRolesInRealm(keycloak, realm, clientId, realmResource.roles(), rolesToCreate);
- List rolesToBind = realmResource.roles().list().stream()
- .filter(roleInRealm -> requestedRealmRoles.contains(roleInRealm.getName()))
- .collect(Collectors.toList());
+ this.serviceAccountRoleAssignmentSynchronizer.synchronizeServiceAccountRealmRoles(realmResource, clientResource, clientUuid);
- serviceAccountRolesMapping
- .realmLevel()
- .add(rolesToBind);
- }
-
- private void createRolesInRealm(String keycloak, String realm, String clientId, RolesResource rolesResource, List rolesToCreate) {
- for (String roleToCreate : rolesToCreate) {
- var representation = new RoleRepresentation();
- representation.setName(roleToCreate);
- representation.setClientRole(false);
- representation.setComposite(false);
- rolesResource.create(representation);
- log.info("{}/{}/{}: created realm role {}", keycloak, realm, clientId, roleToCreate);
- }
- }
-
- private void removeRoleMappingNotRequestedAnymore(String keycloak, String realm, String clientId, RoleMappingResource serviceAccountRoleMapping, List requestedRealmRoles) {
- List rolesToRemove = new ArrayList();
-
- List currentlyMappedRealmRoles = serviceAccountRoleMapping.getAll().getRealmMappings();
- for (RoleRepresentation currentlyMappedRole : currentlyMappedRealmRoles) {
- if (!requestedRealmRoles.contains(currentlyMappedRole.getName())) {
- rolesToRemove.add(currentlyMappedRole);
- log.info("{}/{}/{}: deleted role not requested anymore {}",
- keycloak, realm, clientId, currentlyMappedRole.getName());
- }
- }
- serviceAccountRoleMapping
- .realmLevel()
- .remove(rolesToRemove);
}
private void manageRoles(RealmResource realmResource, String clientUuid, ClientResource clientResource) {
diff --git a/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignment.java b/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignment.java
new file mode 100644
index 0000000..a8afa69
--- /dev/null
+++ b/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignment.java
@@ -0,0 +1,101 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.inject.Singleton;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleMappingResource;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ServiceAccountRoleAssignment {
+ private final Logger LOG = LoggerFactory.getLogger(getClass());
+
+ public List findAssignedRolesToRemoveWith(RealmResource realmResource,
+ ClientResource clientResource, String clientUuid)
+ {
+ List assignedServiceAccountRoles = getAssignedServiceAccountRoles(
+ realmResource,
+ clientUuid);
+ List requestedServiceAccountRoleNames = getRequestedServiceAccountRoleNamesFrom(
+ clientResource);
+
+ return assignedServiceAccountRoles
+ .stream()
+ .filter(roleRepresentation -> !requestedServiceAccountRoleNames
+ .stream()
+ .anyMatch(roleName -> roleName.equalsIgnoreCase(roleRepresentation.getName())))
+ .collect(Collectors.toList());
+ }
+
+ private List getRequestedServiceAccountRoleNamesFrom(ClientResource clientResourceDefinition) {
+ return clientResourceDefinition
+ .getSpec()
+ .getServiceAccountRealmRoles();
+ }
+
+ private List getAssignedServiceAccountRoles(RealmResource realmResource, String clientUuid) {
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = realmResource.clients()
+ .get(clientUuid);
+ return realmResource.users()
+ .get(keycloakClientResource.getServiceAccountUser().getId())
+ .roles().getAll().getRealmMappings();
+ }
+
+ public List findRolesToAssignWith(RealmResource realmResource,
+ ClientResource clientResourceDefinition, String clientUuid)
+ {
+ List assignedServiceAccountRoles = getAssignedServiceAccountRoles(
+ realmResource,
+ clientUuid);
+ List requestedServiceAccountRoleNames = getRequestedServiceAccountRoleNamesFrom(
+ clientResourceDefinition);
+ List serviceAccountRealmRoles = realmResource.roles().list();
+
+ var unassignedRequestedRoleNames = requestedServiceAccountRoleNames.stream()
+ .filter(roleName -> !assignedServiceAccountRoles.stream()
+ .anyMatch(roleRepresentation -> roleName.equalsIgnoreCase(roleRepresentation.getName())))
+ .collect(Collectors.toList());
+
+ return serviceAccountRealmRoles.stream()
+ .filter(roleRepresentation -> unassignedRequestedRoleNames.stream()
+ .anyMatch(roleName -> roleRepresentation.getName().equalsIgnoreCase(roleName)))
+ .collect(Collectors.toList());
+ }
+
+ public List findRequestedRolesToCreateWith(RealmResource realmResource,
+ ClientResource clientResourceDefinition)
+ {
+ List serviceAccountRealmRoleMappings = realmResource.roles().list();
+
+ return getRequestedServiceAccountRoleNamesFrom(clientResourceDefinition)
+ .stream()
+ .filter(roleName -> !serviceAccountRealmRoleMappings
+ .stream()
+ .anyMatch(roleRepresentation -> roleRepresentation
+ .getName()
+ .equalsIgnoreCase(roleName)))
+ .map(this::createRoleRepresentation)
+ .collect(Collectors.toList());
+ }
+
+ RoleRepresentation createRoleRepresentation(String roleName) {
+ var roleRepresentation = new RoleRepresentation();
+ roleRepresentation.setName(roleName);
+ roleRepresentation.setClientRole(false);
+ roleRepresentation.setComposite(false);
+ return roleRepresentation;
+ }
+
+ public RoleMappingResource getServiceAccountRoleMappingsFor(RealmResource realmResource, String clientUuid) {
+ return realmResource.users()
+ .get(realmResource
+ .clients()
+ .get(clientUuid)
+ .getServiceAccountUser()
+ .getId()).roles();
+ }
+}
diff --git a/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizer.java b/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizer.java
new file mode 100644
index 0000000..7eeaac6
--- /dev/null
+++ b/src/main/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizer.java
@@ -0,0 +1,100 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.inject.Singleton;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleMappingResource;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class ServiceAccountRoleAssignmentSynchronizer {
+ private final Logger LOG = LoggerFactory.getLogger(getClass());
+ private final ServiceAccountRoleAssignment serviceAccountRoleAssignment;
+
+ public ServiceAccountRoleAssignmentSynchronizer(ServiceAccountRoleAssignment serviceAccountRoleAssignment) {
+ this.serviceAccountRoleAssignment = serviceAccountRoleAssignment;
+ }
+
+ public void synchronizeServiceAccountRealmRoles(RealmResource realmResource, ClientResource clientResourceDefinition, String clientUuid) {
+ var keycloak = clientResourceDefinition.getSpec().getKeycloak();
+ var realm = clientResourceDefinition.getSpec().getRealm();
+ var clientId = clientResourceDefinition.getSpec().getClientId();
+
+ RoleMappingResource serviceAccountRoleMappings = serviceAccountRoleAssignment.getServiceAccountRoleMappingsFor(
+ realmResource,
+ clientUuid);
+
+ List assignedServiceAccountRolesToRemove = serviceAccountRoleAssignment
+ .findAssignedRolesToRemoveWith(
+ realmResource,
+ clientResourceDefinition,
+ clientUuid);
+
+ if (!assignedServiceAccountRolesToRemove.isEmpty()) {
+ removeAssignedRolesFromServiceAccount(keycloak,
+ realm,
+ clientId,
+ serviceAccountRoleMappings,
+ assignedServiceAccountRolesToRemove);
+ }
+
+ List serviceAccountRealmRolesToCreate = serviceAccountRoleAssignment
+ .findRequestedRolesToCreateWith(
+ realmResource,
+ clientResourceDefinition);
+
+ if (!serviceAccountRealmRolesToCreate.isEmpty()) {
+ createNewRealmRoles(realmResource, keycloak, realm, clientId, serviceAccountRealmRolesToCreate);
+ }
+
+ List serviceAccountRolesToAssign = serviceAccountRoleAssignment
+ .findRolesToAssignWith(
+ realmResource,
+ clientResourceDefinition,
+ clientUuid);
+
+ if (!serviceAccountRolesToAssign.isEmpty()) {
+ assignRequestedRolesToServiceAccount(keycloak,
+ realm,
+ clientId,
+ serviceAccountRoleMappings,
+ serviceAccountRolesToAssign);
+ }
+ }
+
+ private void removeAssignedRolesFromServiceAccount(String keycloak, String realm, String clientId, RoleMappingResource serviceAccountRoleMappings, List assignedServiceAccountRolesToRemove) {
+ serviceAccountRoleMappings.realmLevel().remove(assignedServiceAccountRolesToRemove);
+ LOG.info("{}/{}/{}: deleted roles not requested anymore {}",
+ keycloak,
+ realm,
+ clientId,
+ assignedServiceAccountRolesToRemove.stream()
+ .map(RoleRepresentation::getName)
+ .collect(Collectors.toList()));
+ }
+
+ private void createNewRealmRoles(RealmResource realmResource, String keycloak, String realm, String clientId, List serviceAccountRealmRolesToCreate) {
+ serviceAccountRealmRolesToCreate.stream()
+ .forEach(roleRepresentation -> realmResource.roles().create(roleRepresentation));
+ LOG.info("{}/{}/{}: created realm roles {}",
+ keycloak,
+ realm,
+ clientId,
+ serviceAccountRealmRolesToCreate.stream()
+ .map(RoleRepresentation::getName)
+ .collect(Collectors.toList()));
+ }
+
+ private void assignRequestedRolesToServiceAccount(String keycloak, String realm, String clientId, RoleMappingResource serviceAccountRoleMappings, List serviceAccountRolesToAssign) {
+ serviceAccountRoleMappings.realmLevel().add(serviceAccountRolesToAssign);
+ LOG.info("{}/{}/{}: assigned realm roles {}",
+ keycloak,
+ realm,
+ clientId,
+ serviceAccountRolesToAssign.stream().map(RoleRepresentation::getName).collect(Collectors.toList()));
+ }
+}
diff --git a/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentMocks.java b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentMocks.java
new file mode 100644
index 0000000..d9b0b5f
--- /dev/null
+++ b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentMocks.java
@@ -0,0 +1,85 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.List;
+
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleMappingResource;
+import org.keycloak.admin.client.resource.RoleScopeResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.MappingsRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doAnswer;
+
+public class ServiceAccountRoleAssignmentMocks {
+ org.keycloak.admin.client.resource.ClientResource createKeycloakClientResourceMock() {
+ var keycloakClientResource = Mockito.mock(org.keycloak.admin.client.resource.ClientResource.class);
+ var userRepresentation = Mockito.mock(UserRepresentation.class);
+
+ given(keycloakClientResource.getServiceAccountUser())
+ .willReturn(userRepresentation);
+ given(userRepresentation.getId())
+ .willReturn("some-keycloak-service-account-user-id");
+ given(keycloakClientResource.getServiceAccountUser())
+ .willReturn(userRepresentation);
+
+ return keycloakClientResource;
+ }
+
+ RealmResource createKeycloakRealmResourceMockWith(
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource,
+ List serviceAccountRolesToAssign,
+ List serviceAccountRealmRoles)
+ {
+ var keycloakRealmResource = Mockito.mock(RealmResource.class);
+ var keycloakClientsResource = Mockito.mock(ClientsResource.class);
+ var keycloakUsersResource = Mockito.mock(UsersResource.class);
+ var keycloakUserResource = Mockito.mock(UserResource.class);
+ var keycloakRolesResource = Mockito.mock(RolesResource.class);
+ var keycloakRoleMappingResource = Mockito.mock(RoleMappingResource.class);
+ var keycloakRoleMappingsRepresentation = Mockito.mock(MappingsRepresentation.class);
+ var keycloakRoleScopeResource = Mockito.mock(RoleScopeResource.class);
+
+ given(keycloakRealmResource.clients())
+ .willReturn(keycloakClientsResource);
+ given(keycloakClientsResource.get(anyString()))
+ .willReturn(keycloakClientResource);
+ given(keycloakRealmResource.users())
+ .willReturn(keycloakUsersResource);
+ given(keycloakUsersResource.get(anyString()))
+ .willReturn(keycloakUserResource);
+ given(keycloakUserResource.roles())
+ .willReturn(keycloakRoleMappingResource);
+ given(keycloakRoleMappingResource.getAll())
+ .willReturn(keycloakRoleMappingsRepresentation);
+ given(keycloakRoleMappingResource.realmLevel())
+ .willReturn(keycloakRoleScopeResource);
+ given(keycloakRoleMappingsRepresentation.getRealmMappings())
+ .willReturn(serviceAccountRolesToAssign);
+ given(keycloakRealmResource.roles())
+ .willReturn(keycloakRolesResource);
+ given(keycloakRolesResource.list())
+ .willReturn(serviceAccountRealmRoles);
+
+ doAnswer(invocationOnMock -> {
+ RoleRepresentation roleRepresentation = invocationOnMock.getArgument(0);
+ return serviceAccountRealmRoles.add(roleRepresentation);
+ }).when(keycloakRolesResource).create(any(RoleRepresentation.class));
+
+ doAnswer(invocationOnMock -> {
+ List roleRepresentations = invocationOnMock.getArgument(0);
+ return serviceAccountRolesToAssign.addAll(roleRepresentations);
+ }).when(keycloakRoleScopeResource).add(anyList());
+
+ return keycloakRealmResource;
+ }
+}
diff --git a/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizerTest.java b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizerTest.java
new file mode 100644
index 0000000..3da5b25
--- /dev/null
+++ b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentSynchronizerTest.java
@@ -0,0 +1,146 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleMappingResource;
+import org.keycloak.admin.client.resource.RoleScopeResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+public class ServiceAccountRoleAssignmentSynchronizerTest {
+
+ private static final ServiceAccountRoleAssignmentTestObjects testObjects = new ServiceAccountRoleAssignmentTestObjects();
+ private static final String CLIENT_UUID = "some-client-uuid";
+
+ @Test
+ public void givenAssignedRolesAreNotUnassignedAndCreatedAndReassignedIgnoringCase() {
+ // Arrange
+ List serviceAccountRoleNamesToAssign = List.of("hero", "CITIZEN");
+ com.kiwigrid.keycloak.controller.client.ClientResource k8sClientResourceDefinition = testObjects
+ .createK8sClientResourceWith(serviceAccountRoleNamesToAssign);
+
+ var realmResource = mock(RealmResource.class);
+ var rolesResource = mock(RolesResource.class);
+ when(realmResource.roles()).thenReturn(rolesResource);
+
+ var roleMappingResource = mock(RoleMappingResource.class);
+ var roleScopeResource = mock(RoleScopeResource.class);
+ when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
+
+ ServiceAccountRoleAssignment serviceAccountRoleAssignment = mock(ServiceAccountRoleAssignment.class);
+ when(serviceAccountRoleAssignment.getServiceAccountRoleMappingsFor(realmResource, CLIENT_UUID))
+ .thenReturn(roleMappingResource);
+ when(serviceAccountRoleAssignment.findAssignedRolesToRemoveWith(realmResource,
+ k8sClientResourceDefinition,
+ CLIENT_UUID)).thenReturn(Collections.emptyList());
+ when(serviceAccountRoleAssignment.findRolesToAssignWith(realmResource,
+ k8sClientResourceDefinition,
+ CLIENT_UUID)).thenReturn(Collections.emptyList());
+ when(serviceAccountRoleAssignment.findRequestedRolesToCreateWith(realmResource,
+ k8sClientResourceDefinition)).thenReturn(
+ Collections.emptyList());
+
+ ServiceAccountRoleAssignmentSynchronizer synchronizer = new ServiceAccountRoleAssignmentSynchronizer(
+ serviceAccountRoleAssignment);
+
+ // Act
+ synchronizer.synchronizeServiceAccountRealmRoles(realmResource, k8sClientResourceDefinition, CLIENT_UUID);
+
+ // Assert/Verify
+ verifyNoInteractions(rolesResource, roleScopeResource);
+ }
+
+ @Test
+ public void givenUnassignedRoleToAssignThatDoesNotExistInRealmIsCreatedAndAssignedIgnoringCase() {
+ // Arrange
+ var serviceAccountRoleAssignment = mock(ServiceAccountRoleAssignment.class);
+
+ var realmResource = mock(RealmResource.class);
+ var rolesResource = mock(RolesResource.class);
+ when(realmResource.roles()).thenReturn(rolesResource);
+
+ var roleMappingResource = mock(RoleMappingResource.class);
+ var roleScopeResource = mock(RoleScopeResource.class);
+ when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
+
+ var k8sClientResource = testObjects.createK8sClientResourceWith(List.of("CITIZEN", "HERO"));
+
+ when(serviceAccountRoleAssignment.getServiceAccountRoleMappingsFor(realmResource, CLIENT_UUID))
+ .thenReturn(roleMappingResource);
+ when(serviceAccountRoleAssignment.findAssignedRolesToRemoveWith(realmResource, k8sClientResource, CLIENT_UUID))
+ .thenReturn(Collections.emptyList());
+
+ List rolesToAdd = testObjects.toRoleRepresentations(List.of("HERO"));
+ when(serviceAccountRoleAssignment.findRequestedRolesToCreateWith(realmResource, k8sClientResource))
+ .thenReturn(rolesToAdd);
+ when(serviceAccountRoleAssignment.findRolesToAssignWith(realmResource, k8sClientResource, CLIENT_UUID))
+ .thenReturn(rolesToAdd);
+
+ var synchronizer = new ServiceAccountRoleAssignmentSynchronizer(serviceAccountRoleAssignment);
+
+ // Act
+ synchronizer.synchronizeServiceAccountRealmRoles(realmResource, k8sClientResource, CLIENT_UUID);
+
+ // Assert
+ InOrder inOrder = Mockito.inOrder(roleScopeResource, rolesResource);
+
+ inOrder.verify(roleScopeResource, times(0)).remove(anyList());
+ inOrder.verify(rolesResource).create(rolesToAdd.get(0));
+ inOrder.verify(roleScopeResource).add(rolesToAdd);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ public void givenUnassignedRoleToAssignThatExistsInRealmIsNotCreatedButAssignedIgnoringCase() {
+ // Arrange
+ ServiceAccountRoleAssignment serviceAccountRoleAssignment = mock(ServiceAccountRoleAssignment.class);
+ List serviceAccountRoleNamesToAssign = List.of("CITIZEN", "hero");
+ List serviceAccountRolesToAssign = testObjects
+ .toRoleRepresentations(serviceAccountRoleNamesToAssign);
+ com.kiwigrid.keycloak.controller.client.ClientResource k8sClientResourceDefinition = testObjects
+ .createK8sClientResourceWith(serviceAccountRoleNamesToAssign);
+
+ var realmResource = mock(RealmResource.class);
+ var rolesResource = mock(RolesResource.class);
+ when(realmResource.roles()).thenReturn(rolesResource);
+
+ var roleMappingResource = mock(RoleMappingResource.class);
+ var roleScopeResource = mock(RoleScopeResource.class);
+ when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
+
+ when(serviceAccountRoleAssignment.getServiceAccountRoleMappingsFor(realmResource, CLIENT_UUID))
+ .thenReturn(roleMappingResource);
+ when(serviceAccountRoleAssignment.findRolesToAssignWith(realmResource,
+ k8sClientResourceDefinition,
+ CLIENT_UUID)).thenReturn(serviceAccountRolesToAssign);
+ when(serviceAccountRoleAssignment.findAssignedRolesToRemoveWith(realmResource,
+ k8sClientResourceDefinition,
+ CLIENT_UUID)).thenReturn(Collections.emptyList());
+ when(serviceAccountRoleAssignment.findRequestedRolesToCreateWith(realmResource,
+ k8sClientResourceDefinition)).thenReturn(Collections.emptyList());
+
+ ServiceAccountRoleAssignmentSynchronizer synchronizer = new ServiceAccountRoleAssignmentSynchronizer(
+ serviceAccountRoleAssignment);
+
+ // Act
+ synchronizer.synchronizeServiceAccountRealmRoles(realmResource, k8sClientResourceDefinition, CLIENT_UUID);
+
+ InOrder inOrder = Mockito.inOrder(roleScopeResource, rolesResource);
+ inOrder.verify(roleScopeResource, times(0)).remove(anyList());
+ inOrder.verify(rolesResource, times(0)).create(any(RoleRepresentation.class));
+ inOrder.verify(roleScopeResource).add(serviceAccountRolesToAssign);
+ inOrder.verifyNoMoreInteractions();
+ }
+}
diff --git a/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTest.java b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTest.java
new file mode 100644
index 0000000..f4684bc
--- /dev/null
+++ b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTest.java
@@ -0,0 +1,326 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RoleRepresentation;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ServiceAccountRoleAssignmentTest {
+
+ private static final String SOME_CLIENT_UUID = "some-client-uuid";
+ private static final ServiceAccountRoleAssignmentTestObjects testObjects = new ServiceAccountRoleAssignmentTestObjects();
+ private static final ServiceAccountRoleAssignmentMocks mocks = new ServiceAccountRoleAssignmentMocks();
+
+ @Test
+ public void noRoleFoundForRemovalWhenLowerCaseRoleNamesEqualGivenUpperCaseRoleNames() {
+ // Arrange
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("hero", "criminal"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of("hero",
+ "criminal",
+ "citizen"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource realmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("HERO",
+ "CRIMINAL"));
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualAssignedRolesToRemove = serviceAccountRoles.findAssignedRolesToRemoveWith(
+ realmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualAssignedRolesToRemove).isEmpty();
+ }
+
+ @Test
+ public void noRoleFoundForRemovalWhenUpperCaseRoleNamesEqualGivenLowerCaseRoleNames() {
+ // Arrange
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("HERO", "CRIMINAL"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "HERO",
+ "CRIMINAL",
+ "CITIZEN"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientSourceResource = mocks.createKeycloakClientResourceMock();
+ RealmResource realmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientSourceResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("hero",
+ "criminal"));
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualAssignedRolesToRemove = serviceAccountRoles.findAssignedRolesToRemoveWith(
+ realmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualAssignedRolesToRemove).isEmpty();
+ }
+
+ @Test
+ public void rolesWithLowerCaseNamesAreFoundForRemovalWhenGivenWithUpperCaseNames() {
+ // Arrange
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("hero", "criminal"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "hero",
+ "criminal"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("HERO"));
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ var expectedAssignedServiceAccountRoleToRemove = new RoleRepresentation();
+ expectedAssignedServiceAccountRoleToRemove.setName("criminal");
+
+ // Act
+ List actualServiceAccountRoleMappingsToRemove = serviceAccountRoles.findAssignedRolesToRemoveWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualServiceAccountRoleMappingsToRemove)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(expectedAssignedServiceAccountRoleToRemove);
+ }
+
+ @Test
+ public void rolesWithUpperCaseNamesAreFoundForRemovalWhenGivenWithLowerCaseName() {
+ // Arrange
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("HERO", "CRIMINAL"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "HERO",
+ "CRIMINAL",
+ "CITIZEN"));
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("hero"));
+ var expectedServiceAccountRoleRepresentationToRemove = new RoleRepresentation();
+ expectedServiceAccountRoleRepresentationToRemove.setName("CRIMINAL");
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualAssignedServiceAccountRolesToRemove = serviceAccountRoles.findAssignedRolesToRemoveWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualAssignedServiceAccountRolesToRemove)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(expectedServiceAccountRoleRepresentationToRemove);
+ }
+
+ @Test
+ public void rolesWithLowerCaseNamesAreFoundToAssignWhenGivenWithUpperCaseName() {
+ // Arrange
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("citizen"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "hero",
+ "citizen",
+ "criminal"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("CITIZEN",
+ "HERO"));
+ var expectedServiceAccountRoleToAssign = new RoleRepresentation();
+ expectedServiceAccountRoleToAssign.setName("hero");
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRoleMappingsToAssign = serviceAccountRoles.findRolesToAssignWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualServiceAccountRoleMappingsToAssign)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(expectedServiceAccountRoleToAssign);
+ }
+
+ @Test
+ public void rolesWithUpperCaseNamesAreFoundToAssignWhenGivenWithLowerCaseNames() {
+ // Arrange
+ List assignedServiceAccountRoleMappings = testObjects.toRoleRepresentations(List
+ .of("CITIZEN"));
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "HERO",
+ "CITIZEN",
+ "CRIMINAL"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ assignedServiceAccountRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("citizen",
+ "hero"));
+ var expectedServiceAccountRoleToAssign = new RoleRepresentation();
+ expectedServiceAccountRoleToAssign.setName("HERO");
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRoleMappingsToAssign = serviceAccountRoles.findRolesToAssignWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualServiceAccountRoleMappingsToAssign)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactly(expectedServiceAccountRoleToAssign);
+ }
+
+ @Test
+ public void noRolesFoundToAssignWhenNoRoleExistWithGivenName() {
+ List serviceAccountRealmRolesMappings = testObjects.toRoleRepresentations(List.of(
+ "HERO",
+ "CITIZEN"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ Collections.emptyList(),
+ serviceAccountRealmRolesMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("CRIMINAL"));
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRoleMappingsToAssign = serviceAccountRoles.findRolesToAssignWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition,
+ SOME_CLIENT_UUID);
+
+ // Assert
+ assertThat(actualServiceAccountRoleMappingsToAssign)
+ .isEmpty();
+ }
+
+ @Test
+ public void rolesWithLowerCaseNameAreNotFoundForCreationWhenAlreadyExistentWithUpperCaseNames() {
+ // Arrange
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "CITIZEN",
+ "HERO"));
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ Collections.emptyList(),
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("hero",
+ "citizen"));
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRealmRolesMappingsToCreate = serviceAccountRoles.findRequestedRolesToCreateWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition);
+
+ // Assert
+ assertThat(actualServiceAccountRealmRolesMappingsToCreate).isEmpty();
+ }
+
+ @Test
+ public void rolesWithUpperCaseNameAreNotFoundForCreationWhenAlreadyExistentWithLowerCaseNames() {
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(List.of(
+ "citizen",
+ "hero"));
+
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ Collections.emptyList(),
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("CITIZEN",
+ "HERO"));
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRealmRoleMappinsToCreate = serviceAccountRoles.findRequestedRolesToCreateWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition);
+
+ // Assert
+ assertThat(actualServiceAccountRealmRoleMappinsToCreate).isEmpty();
+ }
+
+ @Test
+ public void rolesGivenWithNamesAreFoundToBeCreatedWhenMissingInRealm() {
+ // Arrange
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ Collections.emptyList(),
+ Collections.emptyList());
+
+ List serviceAccountRoleNames = List.of("HERO", "citizen", "CriMInaL");
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(serviceAccountRoleNames);
+
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+ List expectedServiceAccountRolesToCreate = serviceAccountRoleNames.stream()
+ .map(serviceAccountRoles::createRoleRepresentation)
+ .collect(Collectors.toList());
+ // Act
+ List actualServiceAccountRealmRolesToCreate = serviceAccountRoles.findRequestedRolesToCreateWith(
+ keycloakRealmResource,
+ k8sClientResourceDefinition);
+
+ // Assert
+ assertThat(actualServiceAccountRealmRolesToCreate)
+ .usingRecursiveFieldByFieldElementComparator()
+ .containsExactlyElementsOf(expectedServiceAccountRolesToCreate);
+ }
+
+ @Test
+ public void noRoleIsFoundForCreationWhenGivenRolesAreAssigned() {
+ List serviceAccountRealmRoleNames = List.of("HERO", "CITIZEN", "police_president");
+ List serviceAccountRealmRoleMappings = testObjects.toRoleRepresentations(
+ serviceAccountRealmRoleNames);
+
+ org.keycloak.admin.client.resource.ClientResource keycloakClientResource = mocks.createKeycloakClientResourceMock();
+ RealmResource keycloakRealmResource = mocks.createKeycloakRealmResourceMockWith(keycloakClientResource,
+ serviceAccountRealmRoleMappings,
+ serviceAccountRealmRoleMappings);
+
+ ClientResource k8sClientResourceDefinition = testObjects.createK8sClientResourceWith(List.of("HEro",
+ "citizen",
+ "POLICE_president"));
+ var serviceAccountRoles = new ServiceAccountRoleAssignment();
+
+ // Act
+ List actualServiceAccountRoleMappingsToCreate = serviceAccountRoles.findRequestedRolesToCreateWith(
+ keycloakRealmResource, k8sClientResourceDefinition);
+
+ // Assert
+ assertThat(actualServiceAccountRoleMappingsToCreate).isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTestObjects.java b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTestObjects.java
new file mode 100644
index 0000000..6fa6111
--- /dev/null
+++ b/src/test/java/com/kiwigrid/keycloak/controller/client/ServiceAccountRoleAssignmentTestObjects.java
@@ -0,0 +1,49 @@
+package com.kiwigrid.keycloak.controller.client;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RoleMappingResource;
+import org.keycloak.admin.client.resource.RoleScopeResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.MappingsRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.mockito.Mockito;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doAnswer;
+
+public class ServiceAccountRoleAssignmentTestObjects {
+
+ List toRoleRepresentations(
+ List givenServiceAccountRoleNames)
+ {
+ return givenServiceAccountRoleNames
+ .stream()
+ .map(this::toRoleRepresentation)
+ .collect(Collectors.toList());
+ }
+
+ private RoleRepresentation toRoleRepresentation(String nameName) {
+ var keycloakRoleRepresentation = new RoleRepresentation();
+ keycloakRoleRepresentation.setName(nameName);
+ return keycloakRoleRepresentation;
+ }
+
+ com.kiwigrid.keycloak.controller.client.ClientResource createK8sClientResourceWith(List rolesToAssign) {
+ var clientResourceSpecification = new com.kiwigrid.keycloak.controller.client.ClientResource.ClientResourceSpec();
+ clientResourceSpecification.setServiceAccountRealmRoles(rolesToAssign);
+
+ var clientResource = new com.kiwigrid.keycloak.controller.client.ClientResource();
+ clientResource.setSpec(clientResourceSpecification);
+ return clientResource;
+ }
+}