From 773487396f3753ab66f5fdac656e76752b8e87ff Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Wed, 2 Jun 2021 11:26:09 +0200
Subject: [PATCH] OZG-400 implement searching user in keycloak

---
 goofy-server/pom.xml                          | 10 +++++
 .../de/itvsh/goofy/common/ContextService.java |  2 +-
 .../common/datatypes/StringBasedValue.java    |  4 --
 .../itvsh/goofy/common/user/UserIdMapper.java | 15 +++++++
 .../goofy/common/user/UserRemoteService.java  | 12 ++++++
 .../keycloak/KeycloakAdminConfiguration.java  | 39 +++++++++++++++++++
 .../keycloak/KeycloakAdminProperties.java     | 22 +++++++++++
 .../user/keycloak/KeycloakApiService.java     | 29 ++++++++++++++
 .../keycloak/KeycloakUserRemoteService.java   | 31 +++++++++++++++
 .../keycloak/UserRepresentationMapper.java    | 18 +++++++++
 .../src/main/resources/application.yml        |  4 ++
 .../goofy/common/ContextServiceTest.java      |  2 +-
 .../keycloak/KeycloakApiFunctionalITCase.java | 38 ++++++++++++++++++
 pom.xml                                       | 18 ++++++---
 14 files changed, 232 insertions(+), 12 deletions(-)
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/UserIdMapper.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/UserRemoteService.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminConfiguration.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminProperties.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiService.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakUserRemoteService.java
 create mode 100644 goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/UserRepresentationMapper.java
 create mode 100644 goofy-server/src/test/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiFunctionalITCase.java

diff --git a/goofy-server/pom.xml b/goofy-server/pom.xml
index 841b6bb0e2..12f10df6b7 100644
--- a/goofy-server/pom.xml
+++ b/goofy-server/pom.xml
@@ -63,10 +63,15 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-security</artifactId>
 		</dependency>
+		
 		<dependency>
 			<groupId>org.keycloak</groupId>
 			<artifactId>keycloak-spring-boot-starter</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.keycloak</groupId>
+			<artifactId>keycloak-admin-client</artifactId>
+		</dependency>
 
 		<!-- own projects -->
 		<dependency>
@@ -142,6 +147,11 @@
 			<groupId>com.thedeanda</groupId>
 			<artifactId>lorem</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+			<optional>true</optional>
+		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/ContextService.java b/goofy-server/src/main/java/de/itvsh/goofy/common/ContextService.java
index cfd6d28b69..3a73893ec8 100644
--- a/goofy-server/src/main/java/de/itvsh/goofy/common/ContextService.java
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/ContextService.java
@@ -28,7 +28,7 @@ public class ContextService {
 		var goofyUser = userService.getUser();
 
 		return GrpcUser.newBuilder()
-				.setId(goofyUser.getId().toStringValue())
+				.setId(goofyUser.getId().toString())
 				.setName(goofyUser.getFullName())
 				.addAllRoles(goofyUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet()))
 				.build();
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/datatypes/StringBasedValue.java b/goofy-server/src/main/java/de/itvsh/goofy/common/datatypes/StringBasedValue.java
index a2e3d328ca..619b7a199c 100644
--- a/goofy-server/src/main/java/de/itvsh/goofy/common/datatypes/StringBasedValue.java
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/datatypes/StringBasedValue.java
@@ -25,10 +25,6 @@ public abstract class StringBasedValue implements Serializable {
 		this.value = value;
 	}
 
-	public String toStringValue() {
-		return value;
-	}
-
 	@Override
 	public String toString() {
 		return value;
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserIdMapper.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserIdMapper.java
new file mode 100644
index 0000000000..f9349f3404
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserIdMapper.java
@@ -0,0 +1,15 @@
+package de.itvsh.goofy.common.user;
+
+import org.mapstruct.Mapper;
+
+@Mapper
+public interface UserIdMapper {
+
+	default UserId fromString(String userId) {
+		return UserId.from(userId);
+	}
+
+	default String toString(UserId userId) {
+		return userId.toString();
+	}
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserRemoteService.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserRemoteService.java
new file mode 100644
index 0000000000..f9e68ee8f1
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/UserRemoteService.java
@@ -0,0 +1,12 @@
+package de.itvsh.goofy.common.user;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public interface UserRemoteService {
+
+	Optional<GoofyUser> findUser(UserId userId);
+
+	Stream<GoofyUser> findUserProfile(String searchBy, int limit);
+
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminConfiguration.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminConfiguration.java
new file mode 100644
index 0000000000..b8e75f9ed3
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminConfiguration.java
@@ -0,0 +1,39 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.KeycloakBuilder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+class KeycloakAdminConfiguration {
+
+	private static final String CLIENT = "admin-cli";
+
+	@Autowired
+	private KeycloakSpringBootConfigResolver configResolver;
+	@Autowired
+	private KeycloakAdminProperties adminProperties;
+
+	@Bean
+	public KeycloakDeployment keycloakDeployment() {
+		return configResolver.resolve(null);
+	}
+
+	@Bean
+	public Keycloak keycloak() {
+		var deployment = this.keycloakDeployment();
+		String realm = deployment.getRealm();
+
+		return KeycloakBuilder.builder()
+				.serverUrl(deployment.getAuthServerBaseUrl())
+				.realm(realm)
+				.username(adminProperties.getUser())
+				.password(adminProperties.getPassword())
+				.clientId(CLIENT)
+				.build();
+	}
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminProperties.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminProperties.java
new file mode 100644
index 0000000000..71754dd52d
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakAdminProperties.java
@@ -0,0 +1,22 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotBlank;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Valid
+@Configuration
+@ConfigurationProperties(prefix = "goofy.keycloak.api")
+class KeycloakAdminProperties {
+	@NotBlank
+	private String user;
+	@NotBlank
+	private String password;
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiService.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiService.java
new file mode 100644
index 0000000000..58433f0965
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiService.java
@@ -0,0 +1,29 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.itvsh.goofy.common.user.UserId;
+
+@Service
+class KeycloakApiService {
+
+	@Autowired
+	private Keycloak keycloak;
+	@Autowired
+	private KeycloakDeployment deployment;
+
+	public Optional<UserRepresentation> findUser(UserId userId) {
+		return Optional.ofNullable(keycloak.realm(deployment.getRealm()).users().get(userId.toString()).toRepresentation());
+	}
+
+	public Stream<UserRepresentation> findUserProfile(String searchBy, int limit) {
+		return keycloak.realm(deployment.getRealm()).users().search(searchBy, 0, limit).stream();
+	}
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakUserRemoteService.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakUserRemoteService.java
new file mode 100644
index 0000000000..b3f1169d25
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/KeycloakUserRemoteService.java
@@ -0,0 +1,31 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.itvsh.goofy.common.user.GoofyUser;
+import de.itvsh.goofy.common.user.UserId;
+import de.itvsh.goofy.common.user.UserRemoteService;
+
+@Service
+class KeycloakUserRemoteService implements UserRemoteService {
+
+	@Autowired
+	private KeycloakApiService apiService;
+	@Autowired
+	private UserRepresentationMapper userMapper;
+
+	@Override
+	public Optional<GoofyUser> findUser(UserId userId) {
+		return apiService.findUser(userId).map(userMapper::toGoofyUser);
+	}
+
+	@Override
+	public Stream<GoofyUser> findUserProfile(String searchBy, int limit) {
+		return apiService.findUserProfile(searchBy, limit).map(userMapper::toGoofyUser);
+	}
+
+}
diff --git a/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/UserRepresentationMapper.java b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/UserRepresentationMapper.java
new file mode 100644
index 0000000000..d3c1df681f
--- /dev/null
+++ b/goofy-server/src/main/java/de/itvsh/goofy/common/user/keycloak/UserRepresentationMapper.java
@@ -0,0 +1,18 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import org.keycloak.representations.idm.UserRepresentation;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+import de.itvsh.goofy.common.user.GoofyUser;
+import de.itvsh.goofy.common.user.UserIdMapper;
+
+@Mapper(uses = UserIdMapper.class)
+interface UserRepresentationMapper {
+
+	// TODO map roles
+	@Mapping(target = "authorities", ignore = true)
+	@Mapping(target = "authority", ignore = true)
+	@Mapping(target = "userName", source = "username")
+	GoofyUser toGoofyUser(UserRepresentation userRep);
+}
diff --git a/goofy-server/src/main/resources/application.yml b/goofy-server/src/main/resources/application.yml
index 908e7b0925..89eb3f07a2 100644
--- a/goofy-server/src/main/resources/application.yml
+++ b/goofy-server/src/main/resources/application.yml
@@ -26,6 +26,10 @@ management:
 
 goofy:
   production: true
+  keycloak:
+    api:
+      user: goofyApiUser
+      password: abs9354.3Yds
   
   
 grpc:
diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/ContextServiceTest.java b/goofy-server/src/test/java/de/itvsh/goofy/common/ContextServiceTest.java
index 29781b0aad..dee597cc09 100644
--- a/goofy-server/src/test/java/de/itvsh/goofy/common/ContextServiceTest.java
+++ b/goofy-server/src/test/java/de/itvsh/goofy/common/ContextServiceTest.java
@@ -66,7 +66,7 @@ class ContextServiceTest {
 		void shoultHaveUserId() {
 			var context = service.createCallContext();
 
-			assertThat(context.getUser()).extracting(GrpcUser::getId).isEqualTo(UserTestFactory.ID.toStringValue());
+			assertThat(context.getUser()).extracting(GrpcUser::getId).isEqualTo(UserTestFactory.ID.toString());
 		}
 
 		@Test
diff --git a/goofy-server/src/test/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiFunctionalITCase.java b/goofy-server/src/test/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiFunctionalITCase.java
new file mode 100644
index 0000000000..5262344b75
--- /dev/null
+++ b/goofy-server/src/test/java/de/itvsh/goofy/common/user/keycloak/KeycloakApiFunctionalITCase.java
@@ -0,0 +1,38 @@
+package de.itvsh.goofy.common.user.keycloak;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("remotekc")
+class KeycloakApiFunctionalITCase {
+
+	private static final String REALM = "sh-kiel-dev";
+	private static final String TEST_USER_NAME = "beate";
+
+	@Autowired
+	private Keycloak keycloak;
+
+	@Test
+	void shouldFindTestUser() {
+		List<UserRepresentation> users = keycloak.realm(REALM).users().search(TEST_USER_NAME);
+
+		assertThat(users).hasSize(1);
+	}
+
+	@Test
+	void shouldFindTestUserByPartialName() {
+		List<UserRepresentation> users = keycloak.realm(REALM).users().search(TEST_USER_NAME.substring(1, 4));
+
+		assertThat(users).hasSize(1);
+	}
+
+}
diff --git a/pom.xml b/pom.xml
index 1b45cd0bca..521ebdffde 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 		<java.version>11</java.version>
-		
+
 		<pluto.version>${project.version}</pluto.version>
 
 		<spring.boot.version>2.4.5</spring.boot.version>
@@ -27,7 +27,7 @@
 		<spring-admin.version>2.3.1</spring-admin.version>
 		<mapstruct.version>1.4.1.Final</mapstruct.version>
 		<commons-io.version>2.8.0</commons-io.version>
-		<keycloak-adapter.version>12.0.4</keycloak-adapter.version>
+		<keycloak-adapter.version>13.0.1</keycloak-adapter.version>
 
 		<lorem.version>2.1</lorem.version>
 
@@ -50,6 +50,7 @@
 				<type>pom</type>
 				<scope>import</scope>
 			</dependency>
+			
 			<!-- keycloak -->
 			<dependency>
 				<groupId>org.keycloak.bom</groupId>
@@ -58,6 +59,11 @@
 				<type>pom</type>
 				<scope>import</scope>
 			</dependency>
+			<dependency>
+				<groupId>org.keycloak</groupId>
+				<artifactId>keycloak-admin-client</artifactId>
+				<version>${keycloak-adapter.version}</version>
+			</dependency>
 
 			<dependency>
 				<groupId>de.codecentric</groupId>
@@ -147,10 +153,10 @@
 				</plugin>
 
 				<plugin>
-          			<groupId>org.sonarsource.scanner.maven</groupId>
-          			<artifactId>sonar-maven-plugin</artifactId>
-        		  	<version>${sonarqube.version}</version>
-		        </plugin>
+					<groupId>org.sonarsource.scanner.maven</groupId>
+					<artifactId>sonar-maven-plugin</artifactId>
+					<version>${sonarqube.version}</version>
+				</plugin>
 
 				<plugin>
 					<groupId>org.apache.maven.plugins</groupId>
-- 
GitLab