Skip to content
Snippets Groups Projects
Commit 9ad0a106 authored by OZGCloud's avatar OZGCloud
Browse files

OZG-3056: implement user id synchronisation

parent 0029969d
No related branches found
No related tags found
No related merge requests found
Showing
with 259 additions and 21 deletions
......@@ -51,6 +51,7 @@ import lombok.ToString;
public class User {
public static final String EXTERNAL_ID_FIELD = "externalId";
public static final String KEYCLOAK_USER_ID = "keycloakUserId";
public static final String EMAIL_FIELD = "email";
public static final String DELETED_FIELD = "deleted";
public static final String LAST_SYNC_TIMESTAMP_FIELD = "lastSyncTimestamp";
......@@ -65,6 +66,8 @@ public class User {
private ObjectId id;
@JsonIgnore
private String externalId;
@JsonIgnore
private String keycloakUserId;
private String firstName;
private String lastName;
private String username;
......
......@@ -60,6 +60,7 @@ public abstract class UserResourceMapper {
@Mapping(target = "lastName", expression = "java(mapLastName(userRes))")
@Mapping(target = "username", expression = "java(mapUsername(userRes))")
@Mapping(target = "externalId", expression = "java(mapId(userRes))")
@Mapping(target = "keycloakUserId", expression = "java(mapKeycloakUserId(userRes))")
@Mapping(target = "organisationsEinheitIds", expression = "java(mapOrganisationsEinheitIds(userRes))")
@Mapping(target = "roles", expression = "java(mapRoles(userRes))")
@Mapping(target = "lastSyncTimestamp", ignore = true)
......@@ -116,6 +117,10 @@ public abstract class UserResourceMapper {
}
String mapKeycloakUserId(UserResource userRes) {
return userRes.toRepresentation().getId();
}
String mapEmail(UserResource userRes) {
return userRes.toRepresentation().getEmail();
}
......
......@@ -57,8 +57,11 @@ class KeycloakApiService {
@ConfigProperty(name = "keycloak.url")
String keycloakUrl;
public Stream<User> findAllUser() {
return handlingKeycloakException(() -> getAllUserRepresentation().map(this::toUser));
public Stream<UserWithAttributes> findAllUser() {
return handlingKeycloakException(() -> getAllUserRepresentation().map(userRepresentation -> UserWithAttributes.builder()
.user(toUser(userRepresentation))
.keycloakAttributes(userRepresentation.getAttributes())
.build()));
}
Stream<UserRepresentation> getAllUserRepresentation() {
......
......@@ -28,23 +28,24 @@ import java.util.stream.Stream;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.bson.types.ObjectId;
import de.itvsh.kop.common.logging.KopLogging;
import de.itvsh.kop.user.User;
@ApplicationScoped
@KopLogging
public class KeycloakUserRemoteService {
static final String ATTRIBUTE_USER_ID = "userId";
public static final String ATTRIBUTE_NAME_USER_ID = "userId";
@Inject
KeycloakApiService apiService;
public Stream<User> getAllUsers() {
public Stream<UserWithAttributes> getAllUsers() {
return apiService.findAllUser();
}
public void setUserIdAttribute(String keycloakUserId, String userId) {
apiService.setAttribute(keycloakUserId, ATTRIBUTE_USER_ID, userId);
public void setUserIdAttribute(String keycloakUserId, ObjectId userId) {
apiService.setAttribute(keycloakUserId, ATTRIBUTE_NAME_USER_ID, userId.toString());
}
}
\ No newline at end of file
package de.itvsh.kop.user.keycloak;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import de.itvsh.kop.user.common.errorhandling.TechnicalException;
public class KeycloakUtils {
private KeycloakUtils() {
}
public static Optional<String> getSingleAttribute(String attributeName, Map<String, List<String>> attributes) {
if (null == attributes) {
return Optional.empty();
}
var attributeValues = attributes.getOrDefault(attributeName, Collections.emptyList());
if (attributeValues.size() > 1) {
throw new TechnicalException(String.format(
"Ambiguous %s attribute. Found %d should be 1.",
attributeName,
attributeValues.size()));
}
return attributeValues.stream().findFirst();
}
}
package de.itvsh.kop.user.keycloak;
import java.util.List;
import java.util.Map;
import de.itvsh.kop.user.User;
import io.quarkus.runtime.annotations.RegisterForReflection;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
@RegisterForReflection
public class UserWithAttributes {
private User user;
private Map<String, List<String>> keycloakAttributes;
}
......@@ -34,6 +34,8 @@ import de.itvsh.kop.common.logging.KopLogging;
import de.itvsh.kop.user.User;
import de.itvsh.kop.user.UserService;
import de.itvsh.kop.user.keycloak.KeycloakUserRemoteService;
import de.itvsh.kop.user.keycloak.KeycloakUtils;
import de.itvsh.kop.user.keycloak.UserWithAttributes;
@ApplicationScoped
@KopLogging
......@@ -48,11 +50,22 @@ class SyncService {
void sync(long syncTimestamp) {
keycloakService.getAllUsers()
.filter(HAS_ANY_ROLE)
.forEach(user -> userService.save(addLastSyncTimestamp(user, syncTimestamp)));
.filter(u -> HAS_ANY_ROLE.test(u.getUser()))
.forEach(userWithAttributes -> {
var saved = userService.save(addLastSyncTimestamp(userWithAttributes.getUser(), syncTimestamp));
updateUserIdAttributeInKeycloak(userWithAttributes.toBuilder().user(saved).build());
});
userService.markUnsyncedUsersAsDeleted(syncTimestamp);
}
private void updateUserIdAttributeInKeycloak(UserWithAttributes user) {
var currentUserIdInKeycloak = KeycloakUtils.getSingleAttribute(KeycloakUserRemoteService.ATTRIBUTE_NAME_USER_ID,
user.getKeycloakAttributes());
var currentUserId = user.getUser().getId().toString();
if (currentUserIdInKeycloak.isEmpty() || !currentUserIdInKeycloak.get().equals(currentUserId)) {
keycloakService.setUserIdAttribute(user.getUser().getKeycloakUserId(), user.getUser().getId());
}
}
private User addLastSyncTimestamp(User user, long syncTimestamp) {
......
......@@ -168,6 +168,13 @@ class UserResourceMapperTest {
assertThat(user.getRoles()).isNotEmpty().contains(UserRepresentationTestFactory.ROLE_NAME);
}
@Test
void shouldMapKeycloakUserId() {
var user = toKopUser();
assertThat(user.getKeycloakUserId()).isEqualTo(UserRepresentationTestFactory.EXTERNAL_ID_FALLBACK);
}
private User toKopUser() {
return toKopUser(UserResourceTestFactory.create());
}
......
......@@ -35,6 +35,7 @@ import de.itvsh.kop.user.settings.UserSettingsTestFactory;
public class UserTestFactory {
public static final ObjectId ID = new ObjectId();
public static final String KEYCLOAK_USER_ID = UUID.randomUUID().toString();
public static final String ID_STR = ID.toHexString();
public static final String FIRST_NAME = LoremIpsum.getInstance().getFirstName();
......@@ -53,6 +54,7 @@ public class UserTestFactory {
public static User.UserBuilder createBuilder() {
return User.builder()
.id(ID)
.keycloakUserId(KEYCLOAK_USER_ID)
.firstName(FIRST_NAME)
.lastName(LAST_NAME)
.username(USER_NAME)
......
......@@ -25,6 +25,7 @@ package de.itvsh.kop.user.keycloak;
import static org.mockito.Mockito.*;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
......@@ -54,14 +55,14 @@ class KeycloakUserRemoteServiceTest {
@Nested
class TestSetUserIdAttribute {
private static final String USER_ID = "1ae2-b123";
private static final String ATTRIBUTE_VALUE_USER_ID = "10";
private static final String KEYCLOAK_USER_ID = "1ae2-b123";
private static final ObjectId USER_ID = new ObjectId("deadbeefdeadbeefdeadbeef");
@Test
void shouldCallApiService() {
remoteService.setUserIdAttribute(USER_ID, ATTRIBUTE_VALUE_USER_ID);
remoteService.setUserIdAttribute(KEYCLOAK_USER_ID, USER_ID);
verify(apiService).setAttribute(USER_ID, KeycloakUserRemoteService.ATTRIBUTE_USER_ID, ATTRIBUTE_VALUE_USER_ID);
verify(apiService).setAttribute(KEYCLOAK_USER_ID, KeycloakUserRemoteService.ATTRIBUTE_NAME_USER_ID, USER_ID.toString());
}
}
}
package de.itvsh.kop.user.keycloak;
import static org.assertj.core.api.Assertions.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import de.itvsh.kop.user.common.errorhandling.TechnicalException;
public class KeycloakUtilsTest {
@Nested
class TestGetSingleAttribute {
private static final String ATTRIBUTE_NAME = "userId";
private static final String ATTRIBUTE_VALUE = "abc";
@Test
void shouldReturnAttribute() {
var attributes = new HashMap<String, List<String>>();
attributes.put(ATTRIBUTE_NAME, Collections.singletonList(ATTRIBUTE_VALUE));
var attribute = KeycloakUtils.getSingleAttribute(ATTRIBUTE_NAME, attributes);
assertThat(attribute)
.isNotEmpty()
.contains(ATTRIBUTE_VALUE);
}
@Test
void shouldReturnEmpty() {
var attributes = new HashMap<String, List<String>>();
var attribute = KeycloakUtils.getSingleAttribute(ATTRIBUTE_NAME, attributes);
assertThat(attribute).isEmpty();
}
@Test
void shouldReturnEmptyForNullAttributes() {
assertThat(KeycloakUtils.getSingleAttribute(ATTRIBUTE_NAME, null)).isEmpty();
}
@Test
void shouldThrowTechnicalException() {
var attributes = new HashMap<String, List<String>>();
attributes.put(ATTRIBUTE_NAME, Arrays.asList(ATTRIBUTE_VALUE, ATTRIBUTE_VALUE));
assertThatCode(() -> KeycloakUtils.getSingleAttribute(ATTRIBUTE_NAME, attributes))
.isInstanceOf(TechnicalException.class)
.hasMessageContaining(String.format("Ambiguous %s attribute. Found 2 should be 1.", ATTRIBUTE_NAME));
}
}
}
package de.itvsh.kop.user.keycloak;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.itvsh.kop.user.User;
import de.itvsh.kop.user.UserTestFactory;
public class UserWithAttributesTestFactory {
public static final User USER = UserTestFactory.create();
public static final String ATTRIBUTE_KEY = "userId";
public static final String ATTRIBUTE_VALUE = "deadbeefdeadbeefdeadbeef";
public static final Map<String, List<String>> ATTRIBUTES = new HashMap<>();
static {
ATTRIBUTES.put(ATTRIBUTE_KEY, Collections.singletonList(ATTRIBUTE_VALUE));
}
public static UserWithAttributes create() {
return createBuilder().build();
}
public static UserWithAttributes.UserWithAttributesBuilder createBuilder() {
return UserWithAttributes.builder()
.user(USER)
.keycloakAttributes(ATTRIBUTES);
}
}
......@@ -7,8 +7,8 @@ import static org.mockito.Mockito.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.inject.Inject;
......@@ -28,6 +28,7 @@ import de.itvsh.kop.user.UserTestFactory;
import de.itvsh.kop.user.common.MongoDbTestProfile;
import de.itvsh.kop.user.common.lock.Lock;
import de.itvsh.kop.user.keycloak.KeycloakUserRemoteService;
import de.itvsh.kop.user.keycloak.UserWithAttributesTestFactory;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.mockito.InjectMock;
......@@ -55,7 +56,7 @@ class SyncSchedulerITCase {
@BeforeEach
void init() {
when(keycloakUserRemoteService.getAllUsers()).thenReturn(List.of(UserTestFactory.create()).stream());
when(keycloakUserRemoteService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.create()));
mongoDb = TestDatabaseUtils.getDatabase(mongoClient);
mongoDb.drop();
......@@ -67,7 +68,9 @@ class SyncSchedulerITCase {
User user1 = UserTestFactory.createBuilder().externalId("aaaaa").build();
User user2 = UserTestFactory.createBuilder().externalId("bbbbb").build();
persist(user1, user2);
when(keycloakUserRemoteService.getAllUsers()).thenReturn(List.of(user1, user2).stream());
when(keycloakUserRemoteService.getAllUsers()).thenReturn(Stream.of(
UserWithAttributesTestFactory.createBuilder().user(user1).build(),
UserWithAttributesTestFactory.createBuilder().user(user2).build()));
}
private void persist(User... users) {
......
......@@ -30,6 +30,7 @@ import static org.mockito.Mockito.*;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import javax.inject.Inject;
......@@ -45,6 +46,7 @@ import de.itvsh.kop.user.UserService;
import de.itvsh.kop.user.UserTestFactory;
import de.itvsh.kop.user.common.MongoDbTestProfile;
import de.itvsh.kop.user.keycloak.KeycloakUserRemoteService;
import de.itvsh.kop.user.keycloak.UserWithAttributesTestFactory;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.mockito.InjectMock;
......@@ -71,7 +73,7 @@ class SyncServiceITCase {
@BeforeEach
void init() {
when(keycloakUserRemoteService.getAllUsers()).thenReturn(List.of(UserTestFactory.create()).stream());
when(keycloakUserRemoteService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.create()));
mongoDb = TestDatabaseUtils.getDatabase(mongoClient);
mongoDb.drop();
......@@ -93,7 +95,8 @@ class SyncServiceITCase {
private void prepareKeycloakUserWithoutRoles() {
User user = UserTestFactory.create();
when(keycloakUserRemoteService.getAllUsers()).thenReturn(List.of(user.toBuilder().roles(List.of()).build()).stream());
when(keycloakUserRemoteService.getAllUsers()).thenReturn(
Stream.of(UserWithAttributesTestFactory.createBuilder().user(user.toBuilder().roles(List.of()).build()).build()));
}
@Test
......@@ -111,7 +114,10 @@ class SyncServiceITCase {
User user1 = UserTestFactory.createBuilder().externalId("aaaaa").build();
User user2 = UserTestFactory.createBuilder().externalId("bbbbb").build();
persist(user1, user2);
when(keycloakUserRemoteService.getAllUsers()).thenReturn(List.of(user1, user2).stream());
when(keycloakUserRemoteService.getAllUsers()).thenReturn(Stream.of(
UserWithAttributesTestFactory.createBuilder().user(user1).build(),
UserWithAttributesTestFactory.createBuilder().user(user2).build()
));
}
private void persist(User... users) {
......
......@@ -28,6 +28,10 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.time.Instant;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
......@@ -44,6 +48,7 @@ import de.itvsh.kop.user.UserService;
import de.itvsh.kop.user.UserTestFactory;
import de.itvsh.kop.user.common.lock.LockService;
import de.itvsh.kop.user.keycloak.KeycloakUserRemoteService;
import de.itvsh.kop.user.keycloak.UserWithAttributesTestFactory;
class SyncServiceTest {
......@@ -77,7 +82,11 @@ class SyncServiceTest {
@Test
void shouldCallUserServiceSave() {
when(keycloakService.getAllUsers()).thenReturn(Stream.of(user, user));
when(keycloakService.getAllUsers()).thenReturn(
Stream.of(
UserWithAttributesTestFactory.createBuilder().user(user).build(),
UserWithAttributesTestFactory.createBuilder().user(user).build()));
when(userService.save(any())).thenReturn(user);
service.sync(Instant.now().toEpochMilli());
......@@ -86,7 +95,9 @@ class SyncServiceTest {
@Test
void shouldAddLasSyncToUser() {
when(keycloakService.getAllUsers()).thenReturn(Stream.of(user));
when(keycloakService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.createBuilder().user(user).build()));
when(userService.save(any())).thenReturn(user);
var timestamp = Instant.now().toEpochMilli();
service.sync(timestamp);
......@@ -103,5 +114,42 @@ class SyncServiceTest {
verify(userService).markUnsyncedUsersAsDeleted(anyLong());
}
@Test
void shouldSetUserIdAttributeInKeycloak() {
when(keycloakService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.createBuilder().user(user).build()));
when(userService.save(any())).thenReturn(user);
service.sync(Instant.now().toEpochMilli());
verify(keycloakService).setUserIdAttribute(user.getKeycloakUserId(), user.getId());
}
@Test
void shouldNotSetUserIdAttributeInKeycloak() {
var attributes = new HashMap<String, List<String>>();
attributes.put(KeycloakUserRemoteService.ATTRIBUTE_NAME_USER_ID, Collections.singletonList(user.getId().toString()));
when(keycloakService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.createBuilder()
.keycloakAttributes(attributes)
.build()));
when(userService.save(any())).thenReturn(user);
service.sync(Instant.now().toEpochMilli());
verify(keycloakService, never()).setUserIdAttribute(user.getKeycloakUserId(), user.getId());
}
@Test
void shouldUpdateUserIdAttributeInKeycloak() {
var attributes = new HashMap<String, List<String>>();
attributes.put(KeycloakUserRemoteService.ATTRIBUTE_NAME_USER_ID, Collections.singletonList(UUID.randomUUID().toString()));
when(keycloakService.getAllUsers()).thenReturn(Stream.of(UserWithAttributesTestFactory.createBuilder()
.keycloakAttributes(attributes)
.build()));
when(userService.save(any())).thenReturn(user);
service.sync(Instant.now().toEpochMilli());
verify(keycloakService).setUserIdAttribute(user.getKeycloakUserId(), user.getId());
}
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment