From 445409c72de0e1c1f14f9b3ecf99863eb61fe177 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Tue, 20 Aug 2024 10:47:01 +0200
Subject: [PATCH] OZG-6472 ad userProfileConfig attribute ozgCloudUserId

---
 .../realm/KeycloakRealmRemoteService.java     | 14 +++
 .../keycloak/realm/KeycloakRealmService.java  | 43 +++++++--
 .../realm/KeycloakRealmRemoteServiceTest.java | 52 ++++++++++-
 .../realm/KeycloakRealmServiceTest.java       | 88 ++++++++++++++++++-
 4 files changed, 183 insertions(+), 14 deletions(-)

diff --git a/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
index 5829cac..f896921 100644
--- a/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteService.java
@@ -29,6 +29,7 @@ import java.util.Optional;
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.userprofile.config.UPConfig;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakException;
@@ -69,4 +70,17 @@ class KeycloakRealmRemoteService {
 		keycloak.realm(realm).roles().create(role);
 	}
 
+	UPConfig getUserProfileConfig(RealmRepresentation realmRepresentation) {
+		return keycloak.realm(realmRepresentation.getRealm())
+				.users()
+				.userProfile()
+				.getConfiguration();
+	}
+
+	void updateUserProfileConfig(RealmRepresentation realmRepresentation, UPConfig userProfileConfig) {
+		keycloak.realm(realmRepresentation.getRealm())
+				.users()
+				.userProfile()
+				.update(userProfileConfig);
+	}
 }
diff --git a/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
index 1ebad4e..f219de3 100644
--- a/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
+++ b/ozgcloud-keycloak-operator/src/main/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmService.java
@@ -24,8 +24,12 @@
 package de.ozgcloud.operator.keycloak.realm;
 
 import java.util.Optional;
+import java.util.Set;
 
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.userprofile.config.UPAttribute;
+import org.keycloak.representations.userprofile.config.UPAttributePermissions;
+import org.keycloak.representations.userprofile.config.UPConfig;
 import org.springframework.stereotype.Component;
 
 import de.ozgcloud.operator.keycloak.KeycloakGenericRemoteService;
@@ -37,21 +41,15 @@ import lombok.extern.log4j.Log4j2;
 @Component
 class KeycloakRealmService {
 
+	public static final String USER_PROFILE_CONFIG_OZGCLOUD_ATTRIBUTE_NAME = "ozgCloudUserId";
+
 	private final KeycloakRealmRemoteService remoteService;
 
 	private final KeycloakRealmMapper mapper;
 
 	private final KeycloakGenericRemoteService keycloakGenericRemoteService;
 
-	void createRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
-		Optional.of(realm)
-				.map(mapper::map)
-				.map(realmRepresentation -> addRealmName(realmRepresentation, realmName))
-				.filter(realmRepresentation -> !keycloakGenericRemoteService.realmExists(realmName))
-				.ifPresent(remoteService::createRealm);
-	}
-
-	public void createOrUpdateRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
+	void createOrUpdateRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
 		keycloakGenericRemoteService.getRealmRepresentation(realmName)
 				.ifPresentOrElse(existingRealm -> updateRealm(existingRealm, realm),
 						() -> createRealm(realm, realmName));
@@ -63,12 +61,25 @@ class KeycloakRealmService {
 			LOG.debug("{}: Updating existing realm...", existingRealm);
 			var realmRepresentation = mapper.update(existingRealm, spec);
 			remoteService.updateRealm(realmRepresentation);
+			addUserProfileAttributes(realmRepresentation);
 		} catch (Exception e) {
 			LOG.warn(existingRealm + ": Updating existing realm failed: ", e);
 			throw e;
 		}
 		addOrUpdateRealmRoles(spec, existingRealm.getRealm());
+	}
 
+	void createRealm(OzgCloudKeycloakRealmSpec realm, String realmName) {
+		Optional.of(realm)
+				.map(mapper::map)
+				.map(realmRepresentation -> addRealmName(realmRepresentation, realmName))
+				// TODO dieser Filter kann vermutlich gelöscht werden, die Prüfung auf
+				// realmExists passiert bereits vorher
+				.filter(realmRepresentation -> !keycloakGenericRemoteService.realmExists(realmName))
+				.ifPresent(realmRepresentation -> {
+					remoteService.createRealm(realmRepresentation);
+					addUserProfileAttributes(realmRepresentation);
+				});
 	}
 
 	void addOrUpdateRealmRoles(OzgCloudKeycloakRealmSpec spec, String realm) {
@@ -91,4 +102,18 @@ class KeycloakRealmService {
 	public boolean realmExists(String realmName) {
 		return keycloakGenericRemoteService.realmExists(realmName);
 	}
+
+	void addUserProfileAttributes(RealmRepresentation realmRepresentation) {
+		UPConfig userProfileConfig = remoteService.getUserProfileConfig(realmRepresentation);
+
+		updateUserProfileConfig(userProfileConfig);
+
+		remoteService.updateUserProfileConfig(realmRepresentation, userProfileConfig);
+	}
+
+	void updateUserProfileConfig(UPConfig userProfileConfig) {
+		userProfileConfig.addOrReplaceAttribute(
+				new UPAttribute(USER_PROFILE_CONFIG_OZGCLOUD_ATTRIBUTE_NAME,
+						new UPAttributePermissions(Set.of("admin", "user"), Set.of("admin", "user"))));
+	}
 }
diff --git a/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
index 0b20c33..83bfaf5 100644
--- a/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmRemoteServiceTest.java
@@ -23,10 +23,10 @@
  */
 package de.ozgcloud.operator.keycloak.realm;
 
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
 
 import java.util.List;
 import java.util.Optional;
@@ -39,7 +39,11 @@ import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.admin.client.resource.RealmsResource;
 import org.keycloak.admin.client.resource.RoleResource;
 import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.admin.client.resource.UserProfileResource;
+import org.keycloak.admin.client.resource.UsersResource;
+import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.userprofile.config.UPConfig;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
@@ -108,7 +112,7 @@ class KeycloakRealmRemoteServiceTest {
 			verify(realmResource).update(realm);
 		}
 	}
-	
+
 	@Nested
 	class TestDeleteRealm {
 
@@ -191,4 +195,44 @@ class KeycloakRealmRemoteServiceTest {
 			}
 		}
 	}
+
+	@Nested
+	class TestUserProfileConfig {
+
+		@Mock
+		RealmRepresentation realmRepresentation;
+
+		@Mock
+		UsersResource usersResource;
+
+		@Mock
+		UserProfileResource userProfileResource;
+
+		@Mock
+		UPConfig userProfileConfig;
+
+		@BeforeEach
+		void init() {
+			when(realmRepresentation.getRealm()).thenReturn(REALM_NAME);
+			when(keycloak.realm(REALM_NAME)).thenReturn(realmResource);
+			when(realmResource.users()).thenReturn(usersResource);
+			when(usersResource.userProfile()).thenReturn(userProfileResource);
+		}
+
+		@Test
+		void getUserProfileConfigshouldReturnKeycloakConfiguration() {
+			when(userProfileResource.getConfiguration()).thenReturn(userProfileConfig);
+
+			var config = remoteService.getUserProfileConfig(realmRepresentation);
+
+			assertThat(config).isSameAs(userProfileConfig);
+		}
+
+		@Test
+		void updateUserProfileConfigshouldCallKeycloakUpdate() {
+			remoteService.updateUserProfileConfig(realmRepresentation, userProfileConfig);
+
+			verify(userProfileResource).update(userProfileConfig);
+		}
+	}
 }
diff --git a/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
index 872e9bb..0513da5 100644
--- a/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
+++ b/ozgcloud-keycloak-operator/src/test/java/de/ozgcloud/operator/keycloak/realm/KeycloakRealmServiceTest.java
@@ -23,6 +23,7 @@
  */
 package de.ozgcloud.operator.keycloak.realm;
 
+import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
@@ -33,6 +34,7 @@ import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.userprofile.config.UPConfig;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
@@ -80,6 +82,7 @@ class KeycloakRealmServiceTest {
 		void shouldCallUpdateRealmIfAlreadyExists() {
 			var existingRealm = RealmRepresentationTestFactory.create();
 			when(keycloakGenericRemoteService.getRealmRepresentation(REALM_NAME)).thenReturn(Optional.of(existingRealm));
+			doNothing().when(service).updateUserProfileConfig(any());
 
 			service.createOrUpdateRealm(REALM, REALM_NAME);
 
@@ -101,6 +104,7 @@ class KeycloakRealmServiceTest {
 		@BeforeEach
 		void init() {
 			when(mapper.update(any(), any())).thenReturn(realmRepresentation);
+			when(remoteService.getUserProfileConfig(realmRepresentation)).thenReturn(new UPConfig());
 		}
 
 		@Test
@@ -126,6 +130,12 @@ class KeycloakRealmServiceTest {
 			verify(service).addOrUpdateRealmRoles(REALM, realmRepresentation.getRealm());
 		}
 
+		@Test
+		void createRealmShouldCallAddAttributes() {
+			service.updateRealm(realmRepresentation, REALM);
+
+			verify(service).addUserProfileAttributes(realmRepresentation);
+		}
 	}
 
 	@DisplayName("Add or Update Realm Roles")
@@ -170,6 +180,7 @@ class KeycloakRealmServiceTest {
 		@BeforeEach
 		void init() {
 			when(mapper.map(REALM)).thenReturn(realmRepresentation);
+			when(remoteService.getUserProfileConfig(realmRepresentation)).thenReturn(new UPConfig());
 		}
 
 		@Test
@@ -197,6 +208,7 @@ class KeycloakRealmServiceTest {
 
 		@Test
 		void shouldNOTCallCreateRealmIfAlreadyExists() {
+			reset(remoteService);
 			when(keycloakGenericRemoteService.realmExists(REALM_NAME)).thenReturn(true);
 
 			service.createRealm(REALM, REALM_NAME);
@@ -212,6 +224,13 @@ class KeycloakRealmServiceTest {
 
 			verify(service).addRealmName(realmRepresentation, REALM_NAME);
 		}
+
+		@Test
+		void updateRealmShouldCallAddAttributes() {
+			service.createRealm(REALM, REALM_NAME);
+
+			verify(service).addUserProfileAttributes(realmRepresentation);
+		}
 	}
 
 	@Test
@@ -232,6 +251,7 @@ class KeycloakRealmServiceTest {
 			verify(remoteService).deleteRealm(REALM_NAME);
 		}
 	}
+
 	@Nested
 	class TestRealmExists {
 
@@ -242,5 +262,71 @@ class KeycloakRealmServiceTest {
 			verify(keycloakGenericRemoteService).realmExists(REALM_NAME);
 		}
 	}
-	
+
+	@Nested
+	class TestAddUserProfileAttributes {
+
+		private UPConfig userProfileConfig;
+
+		@BeforeEach
+		void init() {
+			userProfileConfig = new UPConfig();
+			when(remoteService.getUserProfileConfig(realmRepresentation)).thenReturn(userProfileConfig);
+		}
+
+		@Test
+		void shouldCallRemoteServiceGetUserProfileConfig() {
+			service.addUserProfileAttributes(realmRepresentation);
+
+			verify(remoteService).getUserProfileConfig(realmRepresentation);
+		}
+
+		@Test
+		void shouldCallRemoteServiceUpdateUserProfileConfig() {
+			service.addUserProfileAttributes(realmRepresentation);
+
+			verify(remoteService).updateUserProfileConfig(realmRepresentation, userProfileConfig);
+		}
+
+		@Test
+		void shouldCallUpdateUserProfileConfig() {
+			service.addUserProfileAttributes(realmRepresentation);
+
+			verify(service).updateUserProfileConfig(any(UPConfig.class));
+		}
+	}
+
+	@Nested
+	class TestUpdateUserProfileConfig {
+
+		@Test
+		void shouldAddUserProfileAttribute() {
+			var userProfileConfig = new UPConfig();
+
+			service.updateUserProfileConfig(userProfileConfig);
+
+			assertThat(userProfileConfig.getAttributes()).hasSize(1);
+		}
+
+		@Test
+		void shouldAddUserProfileAttributeOzgCloud() {
+			var userProfileConfig = new UPConfig();
+
+			service.updateUserProfileConfig(userProfileConfig);
+
+			var attributeName = userProfileConfig.getAttributes().get(0).getName();
+			assertThat(attributeName).isEqualTo("ozgCloudUserId");
+		}
+
+		@Test
+		void shouldSetPermissions() {
+			var userProfileConfig = new UPConfig();
+
+			service.updateUserProfileConfig(userProfileConfig);
+
+			var permissions = userProfileConfig.getAttributes().get(0).getPermissions();
+			assertThat(permissions.getEdit()).contains("admin", "user");
+			assertThat(permissions.getView()).contains("admin", "user");
+		}
+	}
 }
-- 
GitLab