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; + } +}