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 a45f6b333265d3acca4531b11ce34fd8950a00d4..53ddefe00b70481000bc5a8566f0e92e209a6f58 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/KeycloakUserService.java @@ -24,16 +24,13 @@ package de.ozgcloud.operator.keycloak.user; import java.util.Optional; -import java.util.logging.Level; -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 lombok.extern.java.Log; +import io.fabric8.kubernetes.api.model.Secret; -@Log @Component class KeycloakUserService { @@ -47,14 +44,10 @@ class KeycloakUserService { private KeycloakUserMapper userMapper; public void createOrUpdateUser(OzgKeycloakUserSpec userSpec, String namespace) { - if (!userSecretService.exists(userSpec, namespace)) { - log.log(Level.INFO, "Update password..."); - var userPassword = userSpec.getKeycloakUser().getPassword(); - var password = StringUtils.isEmpty(userPassword) ? generatePassword() : userPassword; - userSpec.getKeycloakUser().setPassword(password); - log.log(Level.INFO, "Create secret for user: " + userSpec.getKeycloakUser().getUsername()); - userSecretService.create(userSpec, namespace); + if (userSpecHasNoPassword(userSpec, namespace)) { + Secret secret = userSecretService.getOrCreateClusterSecret(userSpec, namespace); + userSpec.getKeycloakUser().setPassword(userSecretService.getPasswordFromSecret(secret)); } remoteService.getUserByName(userSpec.getKeycloakUser().getUsername(), namespace) @@ -62,10 +55,8 @@ class KeycloakUserService { () -> remoteService.createUser(userMapper.map(userSpec), namespace)); } - String generatePassword() { - var upperCaseCharacter = RandomStringUtils.randomAlphabetic(1).toUpperCase(); - var randomString = RandomStringUtils.randomAlphanumeric(7); - return upperCaseCharacter + randomString; + boolean userSpecHasNoPassword(OzgKeycloakUserSpec userSpec, String namespace) { + return StringUtils.isEmpty(userSpec.getKeycloakUser().getPassword()); } public void deleteUser(OzgKeycloakUserSpec userSpec, String namespace) { diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java index 6306aeaca5d216dc94ec28c096c036defc850bd2..1ce5391008ecd85a05188a4b21eac8ecf5d82a64 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilder.java @@ -1,5 +1,6 @@ package de.ozgcloud.operator.keycloak.user; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.stereotype.Component; import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; @@ -14,15 +15,21 @@ class UserSecretBuilder { static final String SECRET_PASSWORD_FIELD = "password"; static final String SECRET_NAME_FIELD = "name"; - public Secret build(String name, KeycloakUserSpecUser userSpec, String namespace) { + Secret build(String name, KeycloakUserSpecUser userSpec, String namespace) { return new SecretBuilder() .withType(SECRET_TYPE) .withMetadata(createMetaData(name, namespace)) .addToStringData(SECRET_NAME_FIELD, userSpec.getUsername()) - .addToStringData(SECRET_PASSWORD_FIELD, userSpec.getPassword()) + .addToStringData(SECRET_PASSWORD_FIELD, generatePassword()) .build(); } + String generatePassword() { + var upperCaseCharacter = RandomStringUtils.randomAlphabetic(1).toUpperCase(); + var randomString = RandomStringUtils.randomAlphanumeric(7); + return upperCaseCharacter + randomString; + } + private ObjectMeta createMetaData(String name, String namespace) { var metadata = new ObjectMeta(); metadata.setName(name); diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java new file mode 100644 index 0000000000000000000000000000000000000000..0b887fbf313cf8f2ddeedab1517b7b7cf9323c65 --- /dev/null +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretReader.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.operator.keycloak.user; + +import org.springframework.stereotype.Component; + +import io.fabric8.kubernetes.api.model.Secret; + +@Component +class UserSecretReader { + + String getPasswortFromSecret(Secret secret) { + return secret.getStringData().get(UserSecretBuilder.SECRET_PASSWORD_FIELD); + } +} diff --git a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java index 4dde6588ef379c2560ca3cc54dc6f12a7e6cb351..09efbd8ad9ec7498cc89009aa751751abaff22c9 100644 --- a/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java +++ b/src/main/java/de/ozgcloud/operator/keycloak/user/UserSecretService.java @@ -1,6 +1,6 @@ package de.ozgcloud.operator.keycloak.user; -import java.util.Objects; +import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -17,19 +17,19 @@ class UserSecretService { @Autowired private UserSecretBuilder secretBuilder; @Autowired + private UserSecretReader secretReader; + @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) { + public Secret create(OzgKeycloakUserSpec userSpec, String namespace) { var secretName = userNameConverter.toSecretName(userSpec.getKeycloakUser()); var credentialsSecret = secretBuilder.build(secretName, userSpec.getKeycloakUser(), namespace); var adapter = createResourceAdpater(getUserSecret(userSpec, namespace)); adapter.create(credentialsSecret); + + return credentialsSecret; } ResourceAdapter<Secret> createResourceAdpater(Resource<Secret> secretResource) { @@ -41,4 +41,13 @@ class UserSecretService { return kubernetesRemoteService.getSecret(namespace, secretName); } + + String getPasswordFromSecret(Secret secret) { + return secretReader.getPasswortFromSecret(secret); + } + + Secret getOrCreateClusterSecret(OzgKeycloakUserSpec userSpec, String namespace) { + return Optional.ofNullable(getUserSecret(userSpec, namespace)).map(Resource::get) + .orElseGet(() -> create(userSpec, namespace)); + } } 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 ef41f004f2d0d7b1195107d4c55cc6dfcfda255e..1924c51b1ee958370878394767beba431db52a33 100644 --- a/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/KeycloakUserServiceTest.java @@ -28,6 +28,7 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.util.Optional; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; @@ -74,26 +75,28 @@ class KeycloakUserServiceTest { private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - @DisplayName("on missing secret") + @DisplayName("user has no password") @Nested - class TestOnMissingSecret { + class TestOnUserHasNoPassword { @BeforeEach void mock() { - when(userSecretService.exists(any(), any())).thenReturn(false); + doReturn(true).when(service).userSpecHasNoPassword(any(), eq(TEST_NAMESPACE)); } @Test - void shouldCreateSecretIfNotExists() { + void shouldGetOrCreateClusterSecret() { service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - verify(userSecretService).create(userSpec, TEST_NAMESPACE); + verify(userSecretService).getOrCreateClusterSecret(userSpec, TEST_NAMESPACE); } @Test - void shouldUpdatePasswordIfNoExists() { + void shouldUpdateUserPassword() { + var password = UUID.randomUUID().toString(); var userWithoutPassword = OzgKeycloakUserSpecTestFactory.createBuilder() .keycloakUser(KeycloakUserSpecUserTestFactory.createBuiler().password(StringUtils.EMPTY).build()).build(); + when(userSecretService.getPasswordFromSecret(any())).thenReturn(password); service.createOrUpdateUser(userWithoutPassword, TEST_NAMESPACE); @@ -102,37 +105,28 @@ class KeycloakUserServiceTest { } } - @DisplayName("generate password") + @DisplayName("on user has password") @Nested - class TestGeneratePassword { + class TestOnUserHasPassword { - @Test - void shouldHaveSize() { - var password = service.generatePassword(); - - assertThat(password).hasSize(8); - } - - @Test - void shouldHaveUpperCaseLetterAtFirst() { - var password = service.generatePassword(); - - assertThat(StringUtils.substring(password, 0, 1)).isUpperCase(); + @BeforeEach + void mock() { + doReturn(false).when(service).userSpecHasNoPassword(any(), any()); } @Test - void shouldContainsAlphanumericOnly() { - var password = service.generatePassword(); + void shouldNotReadSecretFromCluster() { + service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - assertThat(password).isAlphanumeric(); + verify(userSecretService, never()).create(userSpec, TEST_NAMESPACE); } } @Test - void shouldVerifiySecretExists() { + void shouldCallUserHasNoPassword() { service.createOrUpdateUser(userSpec, TEST_NAMESPACE); - verify(userSecretService).exists(userSpec, TEST_NAMESPACE); + verify(service).userSpecHasNoPassword(userSpec, TEST_NAMESPACE); } @Test @@ -175,6 +169,36 @@ class KeycloakUserServiceTest { } } + @DisplayName("Test user has no password") + @Nested + class TestUserHasNoPassword { + + @Test + void testUserHasNoPasswordTrue() { + OzgKeycloakUserSpec user = OzgKeycloakUserSpecTestFactory.create(); + + boolean userHasNoPassword = service.userSpecHasNoPassword(user, TEST_NAMESPACE); + + assertThat(userHasNoPassword).isFalse(); + } + + @Test + void testUserHasNoPasswordFalse() { + OzgKeycloakUserSpec user = createUserWithoutPassword(); + + boolean userHasNoPassword = service.userSpecHasNoPassword(user, TEST_NAMESPACE); + + assertThat(userHasNoPassword).isTrue(); + } + + private OzgKeycloakUserSpec createUserWithoutPassword() { + return OzgKeycloakUserSpecTestFactory.createBuilder() + .keycloakUser(KeycloakUserSpecUserTestFactory.createBuiler() + .password(null).build()) + .build(); + } + } + @Nested class TestDeleteUser { diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java b/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..9d13258d77fb0fe999f3ffabad5b61358f9c6065 --- /dev/null +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/SecretTestFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.operator.keycloak.user; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; + +public class SecretTestFactory { + + public static Secret create() { + return createBuilder().build(); + } + + private static SecretBuilder createBuilder() { + return new SecretBuilder(); + } +} diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java index 1770592461d15be5dc562da0e73d2ac4026fdcaa..2215fbe500827fbf7d08726af4c097640cc575ed 100644 --- a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretBuilderTest.java @@ -1,10 +1,13 @@ package de.ozgcloud.operator.keycloak.user; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.Spy; import de.ozgcloud.operator.keycloak.user.OzgKeycloakUserSpec.KeycloakUserSpecUser; @@ -13,7 +16,8 @@ public class UserSecretBuilderTest { private final static String NAME = "dummyName"; private final static String NAMESPACE = "dummyNamespace"; - private UserSecretBuilder builder = new UserSecretBuilder(); + @Spy + private UserSecretBuilder builder; @DisplayName("Build") @Nested @@ -35,12 +39,21 @@ public class UserSecretBuilderTest { assertThat(secret.getStringData()).containsEntry(UserSecretBuilder.SECRET_NAME_FIELD, KeycloakUserSpecUserTestFactory.USERNAME); } + @Test + void shouldCallGeneratePassword() { + builder.build(NAME, userSpec, NAMESPACE); + + verify(builder).generatePassword(); + } + @Test void shouldHavePassword() { + final String password = "PASSWORD"; + doReturn(password).when(builder).generatePassword(); + var secret = builder.build(NAME, userSpec, NAMESPACE); - assertThat(secret.getStringData()).containsEntry(UserSecretBuilder.SECRET_PASSWORD_FIELD, - KeycloakUserSpecUserTestFactory.PASSWORD); + assertThat(secret.getStringData()).containsEntry(UserSecretBuilder.SECRET_PASSWORD_FIELD, password); } @DisplayName("metadata") @@ -61,5 +74,31 @@ public class UserSecretBuilderTest { assertThat(secret.getMetadata().getNamespace()).isEqualTo(NAMESPACE); } } + + @DisplayName("generate password") + @Nested + class TestGeneratePassword { + + @Test + void shouldHaveSize() { + var password = builder.generatePassword(); + + assertThat(password).hasSize(8); + } + + @Test + void shouldHaveUpperCaseLetterAtFirst() { + var password = builder.generatePassword(); + + assertThat(StringUtils.substring(password, 0, 1)).isUpperCase(); + } + + @Test + void shouldContainsAlphanumericOnly() { + var password = builder.generatePassword(); + + assertThat(password).isAlphanumeric(); + } + } } } diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..283dd464f422779bbbaa89f93a9302487e884776 --- /dev/null +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretReaderTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.operator.keycloak.user; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.mockito.Spy; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; + +class UserSecretReaderTest { + + private static final String PASSWORD = UUID.randomUUID().toString(); + + @Spy + private UserSecretReader reader; + + @Test + void shouldReturnPasssowrd() { + Secret secret = buildSecret(); + + String password = reader.getPasswortFromSecret(secret); + + assertThat(password).isEqualTo(PASSWORD); + } + + private Secret buildSecret() { + return new SecretBuilder().addToStringData(Map.of(UserSecretBuilder.SECRET_PASSWORD_FIELD, PASSWORD)).build(); + } +} diff --git a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java index a69b46d4508d4da9049c60b57770f6af7b5586df..4d93da968265bd54789920e8df48f8319598282c 100644 --- a/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java +++ b/src/test/java/de/ozgcloud/operator/keycloak/user/UserSecretServiceTest.java @@ -28,52 +28,10 @@ class UserSecretServiceTest { @Mock private UserSecretBuilder secretBuilder; @Mock + private UserSecretReader secretReader; + @Mock private KubernetesRemoteService kubernetesRemoteService; - @DisplayName("Exists secret") - @Nested - class TestExistsSecret { - - private final OzgKeycloakUserSpec userSpec = OzgKeycloakUserSpecTestFactory.create(); - - @Mock - private Resource<Secret> resourceMock; - @Mock - private Secret secret; - - @BeforeEach - void mock() { - doReturn(resourceMock).when(userSecretService).getUserSecret(any(), any()); - } - - @Test - void shouldGetUserSecret() { - when(resourceMock.get()).thenReturn(secret); - - userSecretService.exists(userSpec, NAMESPACE); - - verify(userSecretService).getUserSecret(userSpec, NAMESPACE); - } - - @Test - void shouldReturnTrueIfExists() { - when(resourceMock.get()).thenReturn(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 { @@ -128,6 +86,27 @@ class UserSecretServiceTest { verify(resourceAdapter).create(secret); } + + @Test + void shouldReturnCreatedSecret() { + Secret createdSecret = userSecretService.create(userSpec, NAMESPACE); + + assertThat(createdSecret).isNotNull(); + } + } + + @DisplayName("Get password from secret") + @Nested + class TestGetPasswordFromSecret { + + @Test + void shouldCallSecretReader() { + Secret secret = SecretTestFactory.create(); + + userSecretService.getPasswordFromSecret(secret); + + verify(secretReader).getPasswortFromSecret(secret); + } } @DisplayName("Get user secret") @@ -157,4 +136,38 @@ class UserSecretServiceTest { verify(kubernetesRemoteService).getSecret(NAMESPACE, CONVERTED_NAME); } } + + @DisplayName("Get or create cluster secret") + @Nested + class TestGetOrCreateClusterSecret { + + @Mock + private Resource<Secret> secretResource; + + private Secret secret = SecretTestFactory.create(); + + @BeforeEach + void init() { + doReturn(secretResource).when(userSecretService).getUserSecret(any(OzgKeycloakUserSpec.class), eq(NAMESPACE)); + } + + @Test + void shouldReturnExistingSecret() { + when(secretResource.get()).thenReturn(secret); + + Secret secretResponse = userSecretService.getOrCreateClusterSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); + + assertThat(secretResponse).isSameAs(secret); + } + + @Test + void shouldReturnNewSecretIfNotExisting() { + when(secretResource.get()).thenReturn(null); + doReturn(secret).when(userSecretService).create(any(OzgKeycloakUserSpec.class), eq(NAMESPACE)); + + Secret secretResponse = userSecretService.getOrCreateClusterSecret(OzgKeycloakUserSpecTestFactory.create(), NAMESPACE); + + assertThat(secretResponse).isSameAs(secret); + } + } } \ No newline at end of file