From 207d4f7cf372be51af910fb2c6c2094a8fc25a8f Mon Sep 17 00:00:00 2001
From: Felix Reichenbach <felix.reichenbach@mgm-tp.com>
Date: Thu, 9 Jan 2025 17:07:39 +0100
Subject: [PATCH] OZG-6733 OZG-7457 provide keycloak api link

---
 .../admin/common/FeatureToggleProperties.java |   4 +-
 .../admin/keycloak/KeyCloakRootProcessor.java |  59 +++++++
 .../keycloak/KeyCloakRootProcessorITCase.java |  70 ++++++++
 .../keycloak/KeyCloakRootProcessorTest.java   | 154 ++++++++++++++++++
 4 files changed, 286 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessor.java
 create mode 100644 src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorITCase.java
 create mode 100644 src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorTest.java

diff --git a/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java b/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
index 1589df74..71cdb9ee 100644
--- a/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
+++ b/src/main/java/de/ozgcloud/admin/common/FeatureToggleProperties.java
@@ -32,9 +32,11 @@ import lombok.Setter;
 @Setter
 @Getter
 @Configuration
-@ConfigurationProperties(prefix = "ozgcloud.feature")
+@ConfigurationProperties(prefix = FeatureToggleProperties.FEATURE_TOGGLE_PREFIX)
 public class FeatureToggleProperties {
 
+	public static final String FEATURE_TOGGLE_PREFIX = "ozgcloud.feature";
+
 	private boolean postfach;
 	private boolean benutzerRollen;
 	private boolean organisationsEinheiten;
diff --git a/src/main/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessor.java b/src/main/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessor.java
new file mode 100644
index 00000000..de6a05b3
--- /dev/null
+++ b/src/main/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.admin.keycloak;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.hateoas.EntityModel;
+import org.springframework.hateoas.Link;
+import org.springframework.hateoas.server.RepresentationModelProcessor;
+import org.springframework.stereotype.Component;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import de.ozgcloud.admin.Root;
+import de.ozgcloud.admin.common.FeatureToggleProperties;
+import de.ozgcloud.admin.common.user.CurrentUserService;
+import de.ozgcloud.admin.common.user.UserRole;
+import lombok.RequiredArgsConstructor;
+
+@Component
+@RequiredArgsConstructor
+@ConditionalOnProperty(prefix = FeatureToggleProperties.FEATURE_TOGGLE_PREFIX, name = "benutzerRollen", havingValue = "true")
+class KeyCloakRootProcessor implements RepresentationModelProcessor<EntityModel<Root>> {
+
+	public static final String REL_USERS = "users";
+
+	private final KeycloakApiProperties keycloakApiProperties;
+	private final CurrentUserService currentUserService;
+
+	@Override
+	public EntityModel<Root> process(EntityModel<Root> model) {
+		return model.addIf(currentUserService.hasRole(UserRole.ADMIN_ADMIN), () -> Link.of(buildUsersHref(), REL_USERS));
+	}
+
+	String buildUsersHref() {
+		return UriComponentsBuilder.fromUriString(keycloakApiProperties.getUrl())
+				.pathSegment("admin", "realms", keycloakApiProperties.getRealm(), "users") // NOSONAR
+				.build().toUriString();
+	}
+}
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorITCase.java b/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorITCase.java
new file mode 100644
index 00000000..44976f8c
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorITCase.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2025 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.admin.keycloak;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.ApplicationContext;
+
+import de.ozgcloud.common.test.ITCase;
+
+class KeyCloakRootProcessorITCase {
+
+	@Nested
+	@SpringBootTest(properties = {
+			"ozgcloud.feature.benutzerRollen=true"
+	})
+	@ITCase
+	class TestFeatureEnabled {
+
+		@Autowired
+		private ApplicationContext applicationContext;
+
+		@Test
+		void shouldHaveKeyCloakRootProcessorBean() {
+			assertDoesNotThrow(() -> applicationContext.getBean(KeyCloakRootProcessor.class));
+		}
+	}
+
+	@Nested
+	@SpringBootTest(properties = {
+			"ozgcloud.feature.benutzerRollen=false"
+	})
+	@ITCase
+	class TestFeatureDisabled {
+
+		@Autowired
+		private ApplicationContext applicationContext;
+
+		@Test
+		void shouldHaveKeyCloakRootProcessorBean() {
+			assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(KeyCloakRootProcessor.class));
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorTest.java b/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorTest.java
new file mode 100644
index 00000000..604e2281
--- /dev/null
+++ b/src/test/java/de/ozgcloud/admin/keycloak/KeyCloakRootProcessorTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2025 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.admin.keycloak;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.springframework.hateoas.EntityModel;
+import org.springframework.hateoas.Link;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.admin.Root;
+import de.ozgcloud.admin.RootTestFactory;
+import de.ozgcloud.admin.common.user.CurrentUserService;
+import de.ozgcloud.admin.common.user.UserRole;
+
+class KeyCloakRootProcessorTest {
+
+	@InjectMocks
+	@Spy
+	private KeyCloakRootProcessor processor;
+	@Mock
+	private CurrentUserService currentUserService;
+	@Mock
+	private KeycloakApiProperties keycloakApiProperties;
+
+	@Nested
+	class TestProcess {
+		private final String href = LoremIpsum.getInstance().getUrl();
+
+		@Test
+		void shouldCheckUserRole() {
+			processModel();
+
+			verify(currentUserService).hasRole(UserRole.ADMIN_ADMIN);
+		}
+
+		@Nested
+		class TestOnAdminRole {
+
+			@BeforeEach
+			void givenHasAdminRole() {
+				when(currentUserService.hasRole(anyString())).thenReturn(true);
+				doReturn(href).when(processor).buildUsersHref();
+			}
+
+			@Test
+			void shouldCallBuildUsersHref() {
+				processModel();
+
+				verify(processor).buildUsersHref();
+			}
+
+			@Test
+			void shouldAddUsersLink() {
+				var model = processModel();
+
+				assertThat(model.getLink(KeyCloakRootProcessor.REL_USERS)).isNotEmpty();
+			}
+
+			@Test
+			void shouldSetHref() {
+				var model = processModel();
+
+				assertThat(model.getLink(KeyCloakRootProcessor.REL_USERS))
+						.get()
+						.extracting(Link::getHref)
+						.isEqualTo(href);
+			}
+		}
+
+		@Nested
+		class TestOnWrongUserRole {
+
+			@BeforeEach
+			void givenHasWrongRole() {
+				when(currentUserService.hasRole(anyString())).thenReturn(false);
+			}
+
+			@Test
+			void shouldNotAddAnyLinks() {
+				var model = processModel();
+
+				assertThat(model.getLinks()).isEmpty();
+			}
+		}
+
+		private EntityModel<Root> processModel() {
+			return processor.process(EntityModel.of(RootTestFactory.create()));
+		}
+	}
+
+	@Nested
+	class TestBuildUsersHref {
+		private final String baseUrl = "https://keycloak.domain.de";
+		private final String realm = LoremIpsum.getInstance().getWords(1);
+
+		@BeforeEach
+		void mockProperties() {
+			when(keycloakApiProperties.getUrl()).thenReturn(baseUrl);
+			when(keycloakApiProperties.getRealm()).thenReturn(realm);
+		}
+
+		@Test
+		void shouldGetKeyCloakUrl() {
+			processor.buildUsersHref();
+
+			verify(keycloakApiProperties).getUrl();
+		}
+
+		@Test
+		void shouldGetKeyCloakRealm() {
+			processor.buildUsersHref();
+
+			verify(keycloakApiProperties).getRealm();
+		}
+
+		@Test
+		void shouldReturnApiEndpoint() {
+			var uriString = processor.buildUsersHref();
+
+			assertThat(uriString).isEqualTo(baseUrl + "/admin/realms/" + realm + "/users");
+		}
+	}
+}
\ No newline at end of file
-- 
GitLab