diff --git a/pom.xml b/pom.xml index 53c904ec7218ac35a46a93b8500a34490a3f60ee..8f9a65e0d4fcfcacc3df215290cc81d683f37223 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> - <version>3.1.1</version> + <version>3.1.3</version> <relativePath/> </parent> @@ -17,12 +17,16 @@ <description>OZG Cloud Operator</description> <properties> - <operator-sdk.version>5.0.0</operator-sdk.version> <spring-boot.build-image.imageName>docker.ozg-sh.de/ozg-operator:build-latest</spring-boot.build-image.imageName> + + <operator-sdk.version>5.2.0</operator-sdk.version> <mapstruct.version>1.5.5.Final</mapstruct.version> <keycloak-adapter.version>20.0.5</keycloak-adapter.version> + <commons-beanutils.version>1.9.4</commons-beanutils.version> + <reflections.version>0.10.2</reflections.version> + <validation-api.version>2.0.1.Final</validation-api.version> </properties> - + <dependencies> <dependency> <groupId>org.springframework.boot</groupId> @@ -46,7 +50,7 @@ <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> - <version>2.0.1.Final</version> + <version>${validation-api.version}</version> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> @@ -59,7 +63,12 @@ <dependency> <groupId>org.reflections</groupId> <artifactId>reflections</artifactId> - <version>0.10.2</version> + <version>${reflections.version}</version> + </dependency> + <dependency> + <groupId>commons-beanutils</groupId> + <artifactId>commons-beanutils</artifactId> + <version>${commons-beanutils.version}</version> </dependency> <dependency> diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java index fd1d383a7416816a832553b23b4bf82c4229c72b..4e1d2f02fbfb611f550e9682ef5b0dedd7224d66 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteService.java @@ -24,13 +24,8 @@ package de.ozgcloud.operator.keycloak.user; import java.util.Arrays; -import java.util.Base64; -import java.util.Objects; import java.util.Optional; -import java.util.logging.Level; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.keycloak.admin.client.CreatedResponseUtil; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.RealmResource; @@ -43,28 +38,13 @@ import org.springframework.stereotype.Component; import de.ozgcloud.operator.keycloak.KeycloakException; import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService; import de.ozgcloud.operator.keycloak.KeycloakResultParser; -import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; -import io.fabric8.kubernetes.api.model.ObjectMeta; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.extension.ResourceAdapter; -import lombok.extern.java.Log; - -@Log + @Component class KeycloakUserRemoteService { - private static final String SECRET_TYPE = "Opaque"; - static final String SECRET_PASSWORD_FIELD = "password"; - static final String SECRET_NAME_FIELD = "name"; - - private static final String USER_NAME_VALIDITY_REGEX = "[^a-zA-Z0-9]"; - @Autowired private Keycloak keycloak; - @Autowired - private KubernetesRemoteService kubernetesRemoteService; + @Autowired private KeycloakGenericRemoteService keycloakGenericRemoteService; @@ -119,73 +99,4 @@ class KeycloakUserRemoteService { void addClientRoleToUser(RoleRepresentation clientRole, RealmResource realmResource, String userId, ClientRepresentation appClient) { realmResource.users().get(userId).roles().clientLevel(appClient.getId()).add(Arrays.asList(clientRole)); } - - public boolean existSecret(OzgKeycloakUserSpec userSpec, String namespace) { - return Objects.nonNull(getUserSecret(userSpec, namespace).get()); - } - - public void createSecret(OzgKeycloakUserSpec userSpec, String namespace) { - log.log(Level.INFO, "Create secret for user: " + userSpec.getKeycloakUser().getUsername()); - var credentialsSecret = createUserSecret(userSpec.getKeycloakUser(), namespace); - - var adapter = createResourceAdpater(getUserSecret(userSpec, namespace)); - adapter.create(credentialsSecret); - } - - ResourceAdapter<Secret> createResourceAdpater(Resource<Secret> secretResource) { - return new ResourceAdapter<>(secretResource); - } - - Secret createUserSecret(KeycloakUserSpecUser userSpec, String namespace) { - return new SecretBuilder() - .withType(SECRET_TYPE) - .withMetadata(createMetaData(userSpec, namespace)) - .addToStringData(SECRET_NAME_FIELD, userSpec.getUsername()) - .addToStringData(SECRET_PASSWORD_FIELD, getPassword(userSpec.getPassword())) - .build(); - } - - private ObjectMeta createMetaData(KeycloakUserSpecUser userSpec, String namespace) { - var name = buildCredentialSecretName(userSpec); - var metadata = new ObjectMeta(); - metadata.setName(name); - metadata.setNamespace(namespace); - return metadata; - } - - String getPassword(String userPassword) { - return StringUtils.isEmpty(userPassword) ? generateRandomPasswordForKeycloak() : userPassword; - } - - String generateRandomPasswordForKeycloak() { - log.log(Level.INFO, "Generate password..."); - var upperCaseCharacter = RandomStringUtils.randomAlphabetic(1).toUpperCase(); - var randomString = RandomStringUtils.randomAlphanumeric(7); - return upperCaseCharacter + randomString; - } - - public String getPasswordFromSecret(OzgKeycloakUserSpec userSpec, String namespace) { - return getPasswordFromSecret(getUserSecret(userSpec, namespace)); - } - - public Resource<Secret> getUserSecret(OzgKeycloakUserSpec userSpec, String namespace) { - var secretName = buildCredentialSecretName(userSpec.getKeycloakUser()); - return kubernetesRemoteService.getSecret(namespace, secretName); - } - - private String buildCredentialSecretName(KeycloakUserSpecUser userSpec) { - return clarifyName(userSpec.getUsername().toLowerCase()) + "-credentials"; - } - - String clarifyName(String userName) { - return userName.replaceAll(USER_NAME_VALIDITY_REGEX, StringUtils.EMPTY); - } - - private String getPasswordFromSecret(Resource<Secret> secret) { - return decodeBase64(secret.get().getData().get(SECRET_PASSWORD_FIELD)); - } - - private String decodeBase64(String base64String) { - return new String(Base64.getDecoder().decode(base64String)); - } } \ No newline at end of file diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java index dc9223feebb4f52028f6538c3331746f5f416ee6..cbd0038e67fb69cedc2d9269677aab26b870dd05 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java @@ -38,20 +38,20 @@ class KeycloakUserService { @Autowired private KeycloakUserRemoteService remoteService; + @Autowired + private UserSecretService userSecretService; + @Autowired private KeycloakUserMapper userMapper; public void createOrUpdateUser(OzgKeycloakUserSpec userSpec, String namespace) { - log.log(Level.INFO, "createOrUpdateUser"); + if (!userSecretService.exists(userSpec, namespace)) { + userSecretService.create(userSpec, namespace); - if (!remoteService.existSecret(userSpec, namespace)) { - log.log(Level.INFO, "Secret not exists -> create one"); - remoteService.createSecret(userSpec, namespace); + log.log(Level.INFO, "Update password..."); + var password = userSecretService.getPassword(userSpec, namespace); + userSpec.getKeycloakUser().setPassword(password); } - log.log(Level.INFO, "Secret exists, set password"); - var password = remoteService.getPasswordFromSecret(userSpec, namespace); - log.log(Level.INFO, "Update password to: " + password); - userSpec.getKeycloakUser().setPassword(password); remoteService.getUserByName(userSpec.getKeycloakUser().getUsername(), namespace) .ifPresentOrElse(existingUser -> remoteService.updateUser(userMapper.update(existingUser, userSpec), namespace), diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java index bcce1bb38811ef6cd0016a41e1d5d1e67913d39f..c865115328e08f8c576fe0337b20a8594fdd4a6f 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KubernetesRemoteService.java @@ -1,7 +1,5 @@ package de.ozgcloud.operator.keycloak.user; -import java.util.logging.Level; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -18,7 +16,7 @@ class KubernetesRemoteService { private KubernetesClient kubernetesClient; public Resource<Secret> getSecret(String namespace, String name) { - log.log(Level.INFO, "Get " + name + " secret from " + namespace + " namespace."); + log.info(String.format("KubernetesClient: Get %s secret from %s namespace.", name, namespace)); return kubernetesClient.secrets().inNamespace(namespace).withName(name); } } diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java new file mode 100644 index 0000000000000000000000000000000000000000..3b2543e269eef25c243c2455f55787d32f8a0a78 --- /dev/null +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java @@ -0,0 +1,102 @@ +package de.ozgcloud.operator.keycloak.user; + +import java.util.Base64; +import java.util.Objects; +import java.util.logging.Level; + +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.extension.ResourceAdapter; +import lombok.extern.java.Log; + +@Log +@Component +class UserSecretService { + + static final String SECRET_PASSWORD_FIELD = "password"; + static final String SECRET_NAME_FIELD = "name"; + + private static final String SECRET_TYPE = "Opaque"; + private static final String USER_NAME_VALIDITY_REGEX = "[^a-zA-Z0-9]"; + + @Autowired + private KubernetesRemoteService kubernetesRemoteService; + + public boolean exists(OzgKeycloakUserSpec userSpec, String namespace) { + return Objects.nonNull(getUserSecret(userSpec, namespace).get()); + } + + public void create(OzgKeycloakUserSpec userSpec, String namespace) { + log.log(Level.INFO, "Create secret for user: " + userSpec.getKeycloakUser().getUsername()); + var credentialsSecret = createUserSecret(userSpec.getKeycloakUser(), namespace); + + var adapter = createResourceAdpater(getUserSecret(userSpec, namespace)); + adapter.create(credentialsSecret); + } + + ResourceAdapter<Secret> createResourceAdpater(Resource<Secret> secretResource) { + return new ResourceAdapter<>(secretResource); + } + + Secret createUserSecret(KeycloakUserSpecUser userSpec, String namespace) { + return new SecretBuilder() + .withType(SECRET_TYPE) + .withMetadata(createMetaData(userSpec, namespace)) + .addToStringData(SECRET_NAME_FIELD, userSpec.getUsername()) + .addToStringData(SECRET_PASSWORD_FIELD, getPassword(userSpec.getPassword())) + .build(); + } + + private ObjectMeta createMetaData(KeycloakUserSpecUser userSpec, String namespace) { + var name = buildCredentialSecretName(userSpec); + var metadata = new ObjectMeta(); + metadata.setName(name); + metadata.setNamespace(namespace); + return metadata; + } + + String getPassword(String userPassword) { + return StringUtils.isEmpty(userPassword) ? generateRandomPasswordForKeycloak() : userPassword; + } + + String generateRandomPasswordForKeycloak() { + log.log(Level.INFO, "Generate password..."); + var upperCaseCharacter = RandomStringUtils.randomAlphabetic(1).toUpperCase(); + var randomString = RandomStringUtils.randomAlphanumeric(7); + return upperCaseCharacter + randomString; + } + + public String getPassword(OzgKeycloakUserSpec userSpec, String namespace) { + return getPassword(getUserSecret(userSpec, namespace)); + } + + Resource<Secret> getUserSecret(OzgKeycloakUserSpec userSpec, String namespace) { + var secretName = buildCredentialSecretName(userSpec.getKeycloakUser()); + return kubernetesRemoteService.getSecret(namespace, secretName); + } + + private String buildCredentialSecretName(KeycloakUserSpecUser userSpec) { + return clarifyName(userSpec.getUsername().toLowerCase()) + "-credentials"; + } + + String clarifyName(String userName) { + return userName.replaceAll(USER_NAME_VALIDITY_REGEX, StringUtils.EMPTY); + } + + private String getPassword(Resource<Secret> secret) { + return decodeBase64(MapUtils.getString(secret.get().getData(), SECRET_PASSWORD_FIELD)); + } + + private String decodeBase64(String base64String) { + return new String(Base64.getDecoder().decode(base64String)); + } +} diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java b/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java index abe685e6b509dda6ca6503a86fdf387df5fb0eb2..50af3159b3edf1dbccb0de8a036f7e7adfe963e0 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/UserUpdateControlBuilder.java @@ -42,7 +42,7 @@ class UserUpdateControlBuilder { public UpdateControl<OzgKeycloakUser> build() { resource.setStatus(buildOzgKeycloakUserStatus()); - + return buildUpdateControl(); } diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java index 144aa0bde3a0e7734c3ed005c844970e208c89cf..70fd40079ca06c1e92fb78719b2a93322be950a5 100644 --- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserRemoteServiceTest.java @@ -28,20 +28,16 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Optional; import javax.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.RealmResource; @@ -58,11 +54,6 @@ import org.mockito.Spy; import de.ozgcloud.operator.keycloak.KeycloakException; import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService; -import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; -import io.fabric8.kubernetes.api.model.Secret; -import io.fabric8.kubernetes.api.model.SecretBuilder; -import io.fabric8.kubernetes.client.dsl.Resource; -import io.fabric8.kubernetes.client.extension.ResourceAdapter; class KeycloakUserRemoteServiceTest { @@ -71,8 +62,6 @@ class KeycloakUserRemoteServiceTest { private final static String USERID = "UserId"; private final static String CLIENT_ID = "ClientId"; - private final static String NAMESPACE = "dummyNamespace"; - @Spy @InjectMocks private KeycloakUserRemoteService userRemoteService; @@ -283,254 +272,4 @@ class KeycloakUserRemoteServiceTest { verify(usersResource).delete(USERID); } } - - @DisplayName("Exists secret") - @Nested - class TestExistsSecret { - - private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - - @Mock - private Resource<Secret> resourceMock; - - @BeforeEach - void mock() { - doReturn(resourceMock).when(userRemoteService).getUserSecret(any(), any()); - } - - @Test - void shouldGetUserSecret() { - when(resourceMock.get()).thenReturn(new Secret()); - - userRemoteService.existSecret(userSpec, NAMESPACE); - - verify(userRemoteService).getUserSecret(userSpec, NAMESPACE); - } - - @Test - void shouldReturnTrueIfExists() { - when(resourceMock.get()).thenReturn(new Secret()); - - var exists = userRemoteService.existSecret(userSpec, NAMESPACE); - - assertThat(exists).isTrue(); - } - - @Test - void shouldReturnFalseIfNotExists() { - when(resourceMock.get()).thenReturn(null); - - var exists = userRemoteService.existSecret(userSpec, NAMESPACE); - - assertThat(exists).isFalse(); - } - } - - @DisplayName("Create Secret") - @Nested - class TestCreateSecret { - - private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - - @Mock - private Resource<Secret> secretResource; - @Mock - private Secret secret; - @Mock - private ResourceAdapter<Secret> resourceAdapter; - - @BeforeEach - void mock() { - doReturn(secret).when(userRemoteService).createUserSecret(any(), any()); - doReturn(secretResource).when(userRemoteService).getUserSecret(any(), any()); - doReturn(resourceAdapter).when(userRemoteService).createResourceAdpater(any()); - } - - @Test - void shouldBuildUserSecret() { - userRemoteService.createSecret(userSpec, NAMESPACE); - - verify(userRemoteService).createUserSecret(userSpec.getKeycloakUser(), NAMESPACE); - } - - @Test - void shouldGetUserSecret() { - userRemoteService.createSecret(userSpec, NAMESPACE); - - verify(userRemoteService).getUserSecret(userSpec, NAMESPACE); - } - - @Test - void shouldCreateResourcAdpater() { - userRemoteService.createSecret(userSpec, NAMESPACE); - - verify(userRemoteService).createResourceAdpater(secretResource); - } - - @Test - void shouldCreateSecret() { - userRemoteService.createSecret(userSpec, NAMESPACE); - - verify(resourceAdapter).create(secret); - } - } - - @DisplayName("Create user secret") - @Nested - class TestCreateUserSecret { - - private final KeycloakUserSpecUser userSpec = KeycloakUserSpecUserTestFactory.create(); - - @Test - void shouldHaveType() { - var secret = userRemoteService.createUserSecret(userSpec, NAMESPACE); - - assertThat(secret.getType()).isEqualTo("Opaque"); - } - - @Test - void shouldHaveUserName() { - var secret = userRemoteService.createUserSecret(userSpec, NAMESPACE); - - assertThat(secret.getStringData()).containsEntry(KeycloakUserRemoteService.SECRET_NAME_FIELD, KeycloakUserSpecUserTestFactory.USERNAME); - } - - @Test - void shouldHavePassword() { - doReturn(KeycloakUserSpecUserTestFactory.PASSWORD).when(userRemoteService).getPassword(any()); - - var secret = userRemoteService.createUserSecret(userSpec, NAMESPACE); - - assertThat(secret.getStringData()).containsEntry(KeycloakUserRemoteService.SECRET_PASSWORD_FIELD, - KeycloakUserSpecUserTestFactory.PASSWORD); - } - - @DisplayName("metadata") - @Nested - class TestMetaData { - - @Test - void shouldHaveName() { - var secret = userRemoteService.createUserSecret(userSpec, NAMESPACE); - - assertThat(secret.getMetadata().getName()).isEqualTo(userSpec.getUsername() + "-credentials"); - } - - @Test - void shouldHaveNamespace() { - var secret = userRemoteService.createUserSecret(userSpec, NAMESPACE); - - assertThat(secret.getMetadata().getNamespace()).isEqualTo(NAMESPACE); - } - } - } - - @DisplayName("Get password") - @Nested - class TestGetPassword { - - @Test - void shouldReturnPasswordIfExists() { - var password = userRemoteService.getPassword(KeycloakUserSpecUserTestFactory.PASSWORD); - - assertThat(password).isEqualTo(KeycloakUserSpecUserTestFactory.PASSWORD); - } - - @Test - void shouldGeneratePasswordIfNotExists() { - userRemoteService.getPassword(StringUtils.EMPTY); - - verify(userRemoteService).generateRandomPasswordForKeycloak(); - } - - @DisplayName("generate random password for keycloak") - @Nested - class TestGenerateRandomPasswordForKeycloak { - - @Test - void shouldHaveSize() { - var password = userRemoteService.getPassword(StringUtils.EMPTY); - - assertThat(password).hasSize(8); - } - - @Test - void shouldHaveUpperCaseLetterAtFirst() { - var password = userRemoteService.getPassword(StringUtils.EMPTY); - - assertThat(StringUtils.substring(password, 0, 1)).isUpperCase(); - } - - @Test - void shouldContainsAlphanumericOnly() { - var password = userRemoteService.getPassword(StringUtils.EMPTY); - - assertThat(password).isAlphanumeric(); - } - } - } - - @DisplayName("Get password from secret") - @Nested - class TestGetPasswordFromSecret { - - @Mock - private Resource<Secret> resource; - private OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - private Secret secret = new SecretBuilder() - .addToData(KeycloakUserRemoteService.SECRET_PASSWORD_FIELD, Base64.getEncoder().encodeToString("dummyPassword".getBytes())) - .build(); - - @Test - void shouldGetUserSecret() { - doReturn(resource).when(userRemoteService).getUserSecret(any(), any()); - when(resource.get()).thenReturn(secret); - - userRemoteService.getPasswordFromSecret(userSpec, NAMESPACE); - - verify(userRemoteService).getUserSecret(userSpec, NAMESPACE); - } - - @Test - void shouldReturnDecodedPassword() { - doReturn(resource).when(userRemoteService).getUserSecret(any(), any()); - when(resource.get()).thenReturn(secret); - - var password = userRemoteService.getPasswordFromSecret(userSpec, NAMESPACE); - - assertThat(password).isEqualTo("dummyPassword"); - } - } - - @DisplayName("Get user secret") - @Nested - class TestGetUserSecret { - - @Test - void shouldGetSecret() { - userRemoteService.getUserSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); - - verify(kubernetesRemoteService).getSecret(NAMESPACE, KeycloakUserSpecUserTestFactory.USERNAME + "-credentials"); - } - - @Test - void shouldClarifyUserName() { - userRemoteService.getUserSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); - - verify(userRemoteService).clarifyName(KeycloakUserSpecUserTestFactory.USERNAME.toLowerCase()); - } - } - - @DisplayName("Clarify name") - @Nested - class TestClarifyName { - - @ValueSource(strings = { "_user_name_", ".user.name.", "-user-name-" }) - @ParameterizedTest - void shouldReplaceForbiddenCharacter(String userName) { - var clarifiedName = userRemoteService.clarifyName(userName); - - assertThat(clarifiedName).isEqualTo("username"); - } - } } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java index 4bef75990e14e41b6a5076baad07032c525a5950..aa3585c71f3fe6ef7913d589afcf265b93cd4e17 100644 --- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java @@ -58,6 +58,9 @@ class KeycloakUserServiceTest { @Mock private KeycloakUserRemoteService remoteService; + @Mock + private UserSecretService userSecretService; + @Mock private KeycloakUserMapper userMapper; @@ -72,43 +75,47 @@ class KeycloakUserServiceTest { private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - @BeforeEach - void mock() { - when(remoteService.getPasswordFromSecret(any(), any())).thenReturn(PASSWORD); - } + @DisplayName("on missing secret") + @Nested + class TestOnMissingSecret { - @Test - void shouldVerifiySecretExists() { - service.createOrUpdateUser(userSpec, TEST_NAMESPACE); + @BeforeEach + void mock() { + when(userSecretService.getPassword(any(), any())).thenReturn(PASSWORD); + when(userSecretService.exists(any(), any())).thenReturn(false); + } - verify(remoteService).existSecret(userSpec, TEST_NAMESPACE); - } + @Test + void shouldCreateSecretIfNotExists() { + service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - @Test - void shouldCreateSecretIfNotExists() { - when(remoteService.existSecret(any(), any())).thenReturn(false); + verify(userSecretService).create(userSpec, TEST_NAMESPACE); + } - service.createOrUpdateUser(userSpec, TEST_NAMESPACE); + @Test + void shouldGetPasswordFromSecret() { + service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - verify(remoteService).createSecret(userSpec, TEST_NAMESPACE); - } + verify(userSecretService).getPassword(userSpec, TEST_NAMESPACE); + } - @Test - void shouldGetPasswordFromSecret() { - service.createOrUpdateUser(userSpec, TEST_NAMESPACE); + @Test + void shouldSetPasswortFromSecret() { + var userWithoutPassword = OzgKeycloakUserSpecTestFactory.createBuilder() + .keycloakUser(KeycloakUserSpecUserTestFactory.createBuiler().password(StringUtils.EMPTY).build()).build(); - verify(remoteService).getPasswordFromSecret(userSpec, TEST_NAMESPACE); + service.createOrUpdateUser(userWithoutPassword, TEST_NAMESPACE); + + verify(userMapper).map(ozgKeycloakUserSpecCaptor.capture()); + assertThat(ozgKeycloakUserSpecCaptor.getValue().getKeycloakUser().getPassword()).isEqualTo(PASSWORD); + } } @Test - void shouldSetPasswortFromSecret() { - var userWithoutPassword = OzgKeycloakUserSpecTestFactory.createBuilder() - .keycloakUser(KeycloakUserSpecUserTestFactory.createBuiler().password(StringUtils.EMPTY).build()).build(); - - service.createOrUpdateUser(userWithoutPassword, TEST_NAMESPACE); + void shouldVerifiySecretExists() { + service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - verify(userMapper).map(ozgKeycloakUserSpecCaptor.capture()); - assertThat(ozgKeycloakUserSpecCaptor.getValue().getKeycloakUser().getPassword()).isEqualTo(PASSWORD); + verify(userSecretService).exists(userSpec, TEST_NAMESPACE); } @Test diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..48014e1f90fb6195106d81d8294f7c89bce266ee --- /dev/null +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java @@ -0,0 +1,285 @@ +package de.ozgcloud.operator.keycloak.user; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Base64; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; + +import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.dsl.Resource; +import io.fabric8.kubernetes.client.extension.ResourceAdapter; + +class UserSecretServiceTest { + + private final static String NAMESPACE = "dummyNamespace"; + + @Spy + @InjectMocks + private UserSecretService userSecretService; + @Mock + private KubernetesRemoteService kubernetesRemoteService; + + @DisplayName("Exists secret") + @Nested + class TestExistsSecret { + + private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); + + @Mock + private Resource<Secret> resourceMock; + + @BeforeEach + void mock() { + doReturn(resourceMock).when(userSecretService).getUserSecret(any(), any()); + } + + @Test + void shouldGetUserSecret() { + when(resourceMock.get()).thenReturn(new Secret()); + + userSecretService.exists(userSpec, NAMESPACE); + + verify(userSecretService).getUserSecret(userSpec, NAMESPACE); + } + + @Test + void shouldReturnTrueIfExists() { + when(resourceMock.get()).thenReturn(new Secret()); + + var exists = userSecretService.exists(userSpec, NAMESPACE); + + assertThat(exists).isTrue(); + } + + @Test + void shouldReturnFalseIfNotExists() { + when(resourceMock.get()).thenReturn(null); + + var exists = userSecretService.exists(userSpec, NAMESPACE); + + assertThat(exists).isFalse(); + } + } + + @DisplayName("Create Secret") + @Nested + class TestCreateSecret { + + private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); + + @Mock + private Resource<Secret> secretResource; + @Mock + private Secret secret; + @Mock + private ResourceAdapter<Secret> resourceAdapter; + + @BeforeEach + void mock() { + doReturn(secret).when(userSecretService).createUserSecret(any(), any()); + doReturn(secretResource).when(userSecretService).getUserSecret(any(), any()); + doReturn(resourceAdapter).when(userSecretService).createResourceAdpater(any()); + } + + @Test + void shouldBuildUserSecret() { + userSecretService.create(userSpec, NAMESPACE); + + verify(userSecretService).createUserSecret(userSpec.getKeycloakUser(), NAMESPACE); + } + + @Test + void shouldGetUserSecret() { + userSecretService.create(userSpec, NAMESPACE); + + verify(userSecretService).getUserSecret(userSpec, NAMESPACE); + } + + @Test + void shouldCreateResourcAdpater() { + userSecretService.create(userSpec, NAMESPACE); + + verify(userSecretService).createResourceAdpater(secretResource); + } + + @Test + void shouldCreateSecret() { + userSecretService.create(userSpec, NAMESPACE); + + verify(resourceAdapter).create(secret); + } + } + + @DisplayName("Create user secret") + @Nested + class TestCreateUserSecret { + + private final KeycloakUserSpecUser userSpec = KeycloakUserSpecUserTestFactory.create(); + + @Test + void shouldHaveType() { + var secret = userSecretService.createUserSecret(userSpec, NAMESPACE); + + assertThat(secret.getType()).isEqualTo("Opaque"); + } + + @Test + void shouldHaveUserName() { + var secret = userSecretService.createUserSecret(userSpec, NAMESPACE); + + assertThat(secret.getStringData()).containsEntry(UserSecretService.SECRET_NAME_FIELD, KeycloakUserSpecUserTestFactory.USERNAME); + } + + @Test + void shouldHavePassword() { + doReturn(KeycloakUserSpecUserTestFactory.PASSWORD).when(userSecretService).getPassword(any()); + + var secret = userSecretService.createUserSecret(userSpec, NAMESPACE); + + assertThat(secret.getStringData()).containsEntry(UserSecretService.SECRET_PASSWORD_FIELD, + KeycloakUserSpecUserTestFactory.PASSWORD); + } + + @DisplayName("metadata") + @Nested + class TestMetaData { + + @Test + void shouldHaveName() { + var secret = userSecretService.createUserSecret(userSpec, NAMESPACE); + + assertThat(secret.getMetadata().getName()).isEqualTo(userSpec.getUsername() + "-credentials"); + } + + @Test + void shouldHaveNamespace() { + var secret = userSecretService.createUserSecret(userSpec, NAMESPACE); + + assertThat(secret.getMetadata().getNamespace()).isEqualTo(NAMESPACE); + } + } + } + + @DisplayName("Get password") + @Nested + class TestGetPassword { + + @Test + void shouldReturnPasswordIfExists() { + var password = userSecretService.getPassword(KeycloakUserSpecUserTestFactory.PASSWORD); + + assertThat(password).isEqualTo(KeycloakUserSpecUserTestFactory.PASSWORD); + } + + @Test + void shouldGeneratePasswordIfNotExists() { + userSecretService.getPassword(StringUtils.EMPTY); + + verify(userSecretService).generateRandomPasswordForKeycloak(); + } + + @DisplayName("generate random password for keycloak") + @Nested + class TestGenerateRandomPasswordForKeycloak { + + @Test + void shouldHaveSize() { + var password = userSecretService.getPassword(StringUtils.EMPTY); + + assertThat(password).hasSize(8); + } + + @Test + void shouldHaveUpperCaseLetterAtFirst() { + var password = userSecretService.getPassword(StringUtils.EMPTY); + + assertThat(StringUtils.substring(password, 0, 1)).isUpperCase(); + } + + @Test + void shouldContainsAlphanumericOnly() { + var password = userSecretService.getPassword(StringUtils.EMPTY); + + assertThat(password).isAlphanumeric(); + } + } + } + + @DisplayName("Get password from secret") + @Nested + class TestGetPasswordFromSecret { + + @Mock + private Resource<Secret> resource; + private OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); + private Secret secret = new SecretBuilder() + .addToData(UserSecretService.SECRET_PASSWORD_FIELD, Base64.getEncoder().encodeToString("dummyPassword".getBytes())) + .build(); + + @Test + void shouldGetUserSecret() { + doReturn(resource).when(userSecretService).getUserSecret(any(), any()); + when(resource.get()).thenReturn(secret); + + userSecretService.getPassword(userSpec, NAMESPACE); + + verify(userSecretService).getUserSecret(userSpec, NAMESPACE); + } + + @Test + void shouldReturnDecodedPassword() { + doReturn(resource).when(userSecretService).getUserSecret(any(), any()); + when(resource.get()).thenReturn(secret); + + var password = userSecretService.getPassword(userSpec, NAMESPACE); + + assertThat(password).isEqualTo("dummyPassword"); + } + } + + @DisplayName("Get user secret") + @Nested + class TestGetUserSecret { + + @Test + void shouldGetSecret() { + userSecretService.getUserSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); + + verify(kubernetesRemoteService).getSecret(NAMESPACE, KeycloakUserSpecUserTestFactory.USERNAME + "-credentials"); + } + + @Test + void shouldClarifyUserName() { + userSecretService.getUserSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); + + verify(userSecretService).clarifyName(KeycloakUserSpecUserTestFactory.USERNAME.toLowerCase()); + } + } + + @DisplayName("Clarify name") + @Nested + class TestClarifyName { + + @ValueSource(strings = { "_user_name_", ".user.name.", "-user-name-" }) + @ParameterizedTest + void shouldReplaceForbiddenCharacter(String userName) { + var clarifiedName = userSecretService.clarifyName(userName); + + assertThat(clarifiedName).isEqualTo("username"); + } + } +}