From b35f3f0e3a3fe65826648233a62882c3119e198c Mon Sep 17 00:00:00 2001 From: achour94 Date: Mon, 22 Dec 2025 17:22:28 +0100 Subject: [PATCH 1/5] Add user identity enrichment functionality and update related DTOs Signed-off-by: achour94 --- .../server/dto/UserIdentitiesResult.java | 27 ++ .../useradmin/server/dto/UserIdentity.java | 18 ++ .../useradmin/server/dto/UserInfos.java | 29 ++- .../server/entity/UserInfosEntity.java | 2 +- .../server/service/UserAdminService.java | 54 +++- .../server/service/UserIdentityService.java | 111 +++++++++ .../server/service/UserInfosService.java | 28 ++- src/main/resources/application-local.yml | 4 +- .../useradmin/server/DtoConverterTest.java | 10 +- .../useradmin/server/NoQuotaTest.java | 2 +- .../useradmin/server/UserAdminTest.java | 8 +- .../server/UserIdentityIntegrationTest.java | 232 ++++++++++++++++++ .../server/service/UserInfosServiceTest.java | 11 +- 13 files changed, 510 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java create mode 100644 src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java create mode 100644 src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java create mode 100644 src/test/java/org/gridsuite/useradmin/server/UserIdentityIntegrationTest.java diff --git a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java new file mode 100644 index 0000000..e51cd8a --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.useradmin.server.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +/** + * DTO representing the batch response from user-identity-server. + * + * @author Achour Berrahma + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record UserIdentitiesResult( + Map data, + Map errors +) { + public UserIdentitiesResult { + data = data != null ? data : Map.of(); + errors = errors != null ? errors : Map.of(); + } +} \ No newline at end of file diff --git a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java new file mode 100644 index 0000000..660448b --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.useradmin.server.dto; + +/** + * DTO representing user identity information from user-identity-server. + * + * @author Achour Berrahma + */ +public record UserIdentity( + String sub, + String firstName, + String lastName +) { } \ No newline at end of file diff --git a/src/main/java/org/gridsuite/useradmin/server/dto/UserInfos.java b/src/main/java/org/gridsuite/useradmin/server/dto/UserInfos.java index e1051a2..8a5ab53 100644 --- a/src/main/java/org/gridsuite/useradmin/server/dto/UserInfos.java +++ b/src/main/java/org/gridsuite/useradmin/server/dto/UserInfos.java @@ -5,14 +5,41 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.gridsuite.useradmin.server.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import java.util.Set; public record UserInfos( String sub, + @JsonInclude(JsonInclude.Include.NON_NULL) + String firstName, + @JsonInclude(JsonInclude.Include.NON_NULL) + String lastName, String profileName, Integer maxAllowedCases, Integer numberCasesUsed, Integer maxAllowedBuilds, Set groups -) { } +) { + /** + * Creates a new UserInfos with identity information (firstName and lastName). + * + * @param identity the user identity containing firstName and lastName + * @return a new UserInfos with identity information merged + */ + public UserInfos withIdentity(UserIdentity identity) { + if (identity == null) { + return this; + } + return new UserInfos( + sub, + identity.firstName(), + identity.lastName(), + profileName, + maxAllowedCases, + numberCasesUsed, + maxAllowedBuilds, + groups + ); + } +} diff --git a/src/main/java/org/gridsuite/useradmin/server/entity/UserInfosEntity.java b/src/main/java/org/gridsuite/useradmin/server/entity/UserInfosEntity.java index 5928ea0..6bafca6 100644 --- a/src/main/java/org/gridsuite/useradmin/server/entity/UserInfosEntity.java +++ b/src/main/java/org/gridsuite/useradmin/server/entity/UserInfosEntity.java @@ -53,7 +53,7 @@ private UserInfos toUserInfos(Integer maxAllowedCases, Integer maxAllowedBuilds) { String profileName = getProfile() == null ? null : getProfile().getName(); Set groupNames = getGroups() == null ? null : getGroups().stream().map(GroupInfosEntity::getName).collect(Collectors.toSet()); - return new UserInfos(getSub(), profileName, maxAllowedCases, numberCasesUsed, maxAllowedBuilds, groupNames); + return new UserInfos(getSub(), null, null, profileName, maxAllowedCases, numberCasesUsed, maxAllowedBuilds, groupNames); } public static UserInfos toDto(@Nullable final UserInfosEntity entity) { diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java index cd9e499..a5bb825 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java @@ -7,13 +7,14 @@ package org.gridsuite.useradmin.server.service; import org.gridsuite.useradmin.server.UserAdminApplicationProps; -import org.gridsuite.useradmin.server.error.UserAdminException; import org.gridsuite.useradmin.server.dto.UserConnection; import org.gridsuite.useradmin.server.dto.UserGroup; +import org.gridsuite.useradmin.server.dto.UserIdentity; import org.gridsuite.useradmin.server.dto.UserInfos; import org.gridsuite.useradmin.server.dto.UserProfile; import org.gridsuite.useradmin.server.entity.UserInfosEntity; import org.gridsuite.useradmin.server.entity.UserProfileEntity; +import org.gridsuite.useradmin.server.error.UserAdminException; import org.gridsuite.useradmin.server.repository.UserGroupRepository; import org.gridsuite.useradmin.server.repository.UserInfosRepository; import org.gridsuite.useradmin.server.repository.UserProfileRepository; @@ -34,6 +35,7 @@ public class UserAdminService { private final AdminRightService adminRightService; private final UserProfileService userProfileService; private final UserGroupService userGroupService; + private final UserIdentityService userIdentityService; private final UserAdminApplicationProps applicationProps; private final UserGroupRepository userGroupRepository; @@ -44,6 +46,7 @@ public UserAdminService(final UserInfosRepository userInfosRepository, final AdminRightService adminRightService, final UserProfileService userProfileService, final UserGroupService userGroupService, + final UserIdentityService userIdentityService, final UserAdminApplicationProps applicationProps, final UserGroupRepository userGroupRepository) { this.userInfosRepository = Objects.requireNonNull(userInfosRepository); @@ -52,6 +55,7 @@ public UserAdminService(final UserInfosRepository userInfosRepository, this.adminRightService = Objects.requireNonNull(adminRightService); this.userProfileService = Objects.requireNonNull(userProfileService); this.userGroupService = Objects.requireNonNull(userGroupService); + this.userIdentityService = Objects.requireNonNull(userIdentityService); this.applicationProps = Objects.requireNonNull(applicationProps); this.userGroupRepository = userGroupRepository; } @@ -63,7 +67,11 @@ private UserInfos toDtoUserInfo(final UserInfosEntity entity) { @Transactional(readOnly = true) public List getUsers() { adminRightService.assertIsAdmin(); - return userInfosRepository.findAll().stream().map(this::toDtoUserInfo).toList(); + List entities = userInfosRepository.findAll(); + List users = entities.stream().map(this::toDtoUserInfo).toList(); + + // Enrich with identity information (firstName, lastName) + return enrichWithIdentities(users); } @Transactional(readOnly = true) @@ -143,7 +151,9 @@ public void recordConnectionAttempt(String sub, boolean isConnectionAccepted) { @Transactional(readOnly = true) public Optional getUser(String sub) { adminRightService.assertIsAdmin(); - return userInfosRepository.findBySub(sub).map(this::toDtoUserInfo); + return userInfosRepository.findBySub(sub) + .map(this::toDtoUserInfo) + .map(this::enrichWithIdentity); } @Transactional(readOnly = true) @@ -215,4 +225,40 @@ private UserProfile createDefaultProfile() { applicationProps.getDefaultMaxAllowedBuilds() ); } -} + + /** + * Enriches a single user with identity information (firstName, lastName). + * Fails silently if identity cannot be fetched. + */ + private UserInfos enrichWithIdentity(UserInfos userInfos) { + if (userInfos == null) { + return null; + } + Optional identity = userIdentityService.getIdentity(userInfos.sub()); + return identity.map(userInfos::withIdentity).orElse(userInfos); + } + + /** + * Enriches a list of users with identity information (firstName, lastName). + * Uses batch fetching for efficiency. Fails silently if identities cannot be fetched. + */ + private List enrichWithIdentities(List users) { + if (users == null || users.isEmpty()) { + return users; + } + + List subs = users.stream() + .map(UserInfos::sub) + .filter(Objects::nonNull) + .toList(); + + Map identities = userIdentityService.getIdentities(subs); + + return users.stream() + .map(user -> { + UserIdentity identity = identities.get(user.sub()); + return identity != null ? user.withIdentity(identity) : user; + }) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java new file mode 100644 index 0000000..455aa78 --- /dev/null +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.useradmin.server.service; + +import org.gridsuite.useradmin.server.dto.UserIdentitiesResult; +import org.gridsuite.useradmin.server.dto.UserIdentity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +/** + * Service for fetching user identity information (firstName, lastName) from user-identity-server. + * All operations fail silently to ensure that identity enrichment doesn't break core functionality. + * + * @author Achour Berrahma {@literal } + */ +@Service +public class UserIdentityService { + + private static final Logger LOGGER = LoggerFactory.getLogger(UserIdentityService.class); + private static final String USER_IDENTITY_API_VERSION = "v1"; + private static final String IDENTITIES_PATH = "/" + USER_IDENTITY_API_VERSION + "/users/identities"; + + private final RestTemplate restTemplate; + private final String userIdentityServerBaseUri; + + public UserIdentityService( + RestTemplate restTemplate, + @Value("${gridsuite.services.user-identity-server.base-uri:http://user-identity-server/}") String userIdentityServerBaseUri) { + this.restTemplate = restTemplate; + this.userIdentityServerBaseUri = userIdentityServerBaseUri; + } + + /** + * Fetches identity information for a single user. + * Fails silently if the service is unavailable or a user is not found. + * + * @param sub the user's subject identifier + * @return Optional containing UserIdentity if found, empty otherwise + */ + public Optional getIdentity(String sub) { + if (sub == null || sub.isBlank()) { + return Optional.empty(); + } + + try { + String url = userIdentityServerBaseUri + IDENTITIES_PATH + "/" + sub; + UserIdentity identity = restTemplate.getForObject(url, UserIdentity.class); + return Optional.ofNullable(identity); + } catch (Exception e) { + LOGGER.warn("Failed to fetch identity for user '{}': {}", sub, e.getMessage()); + LOGGER.debug("Identity fetch error details", e); + return Optional.empty(); + } + } + + /** + * Fetches identity information for multiple users in a single request. + * Fails silently if the service is unavailable. + * + * @param subs collection of user subject identifiers + * @return Map of sub to UserIdentity for successfully fetched identities + */ + public Map getIdentities(Collection subs) { + if (CollectionUtils.isEmpty(subs)) { + return Map.of(); + } + + try { + String url = UriComponentsBuilder.fromUriString(userIdentityServerBaseUri + IDENTITIES_PATH) + .queryParam("subs", String.join(",", subs)) + .toUriString(); + + UserIdentitiesResult result = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference() { } + ).getBody(); + + if (result == null || result.data() == null) { + return Map.of(); + } + + if (!result.errors().isEmpty()) { + LOGGER.debug("Some user identities could not be fetched: {}", result.errors().keySet()); + } + + return result.data(); + } catch (Exception e) { + LOGGER.warn("Failed to fetch identities for {} users: {}", subs.size(), e.getMessage()); + LOGGER.debug("Batch identity fetch error details", e); + return Map.of(); + } + } + +} diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserInfosService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserInfosService.java index a2095b2..4979f16 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserInfosService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserInfosService.java @@ -1,6 +1,7 @@ package org.gridsuite.useradmin.server.service; import org.gridsuite.useradmin.server.UserAdminApplicationProps; +import org.gridsuite.useradmin.server.dto.UserIdentity; import org.gridsuite.useradmin.server.dto.UserInfos; import org.gridsuite.useradmin.server.entity.UserInfosEntity; import org.gridsuite.useradmin.server.entity.UserProfileEntity; @@ -17,13 +18,16 @@ public class UserInfosService { private final UserInfosRepository userInfosRepository; private final DirectoryService directoryService; + private final UserIdentityService userIdentityService; private final UserAdminApplicationProps applicationProps; public UserInfosService(final UserInfosRepository userInfosRepository, final DirectoryService directoryService, + final UserIdentityService userIdentityService, final UserAdminApplicationProps applicationProps) { this.userInfosRepository = Objects.requireNonNull(userInfosRepository); this.directoryService = Objects.requireNonNull(directoryService); + this.userIdentityService = Objects.requireNonNull(userIdentityService); this.applicationProps = Objects.requireNonNull(applicationProps); } @@ -44,16 +48,24 @@ public UserInfos getUserInfo(String sub) { Optional userInfosEntity = getUserInfosEntity(sub); // get number of cases used Integer casesUsed = directoryService.getCasesCount(sub); + + UserInfos userInfos; if (userInfosEntity.isPresent()) { - return toDtoUserInfo(userInfosEntity.get(), casesUsed); + userInfos = toDtoUserInfo(userInfosEntity.get(), casesUsed); + } else { + userInfos = createDefaultUserInfo(sub, casesUsed); } - return createDefaultUserInfo(sub, casesUsed); + + // Enrich with identity information (firstName, lastName) + return enrichWithIdentity(userInfos); } private UserInfos createDefaultUserInfo(String sub, Integer casesUsed) { return new UserInfos( sub, null, + null, + null, applicationProps.getDefaultMaxAllowedCases(), casesUsed, applicationProps.getDefaultMaxAllowedBuilds(), @@ -64,4 +76,16 @@ private UserInfos createDefaultUserInfo(String sub, Integer casesUsed) { private Optional getUserInfosEntity(String sub) { return userInfosRepository.findBySub(sub); } + + /** + * Enriches user info with identity information (firstName, lastName). + * Fails silently if identity cannot be fetched. + */ + private UserInfos enrichWithIdentity(UserInfos userInfos) { + if (userInfos == null) { + return null; + } + Optional identity = userIdentityService.getIdentity(userInfos.sub()); + return identity.map(userInfos::withIdentity).orElse(userInfos); + } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index b4ce9b5..92a61b5 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -12,4 +12,6 @@ spring: gridsuite: services: directory-server: - base-uri: http://localhost:5026 \ No newline at end of file + base-uri: http://localhost:5026 + user-identity-server: + base-uri: http://localhost:5034 \ No newline at end of file diff --git a/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java b/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java index 4dbfc7e..dd2c18a 100644 --- a/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java @@ -24,18 +24,18 @@ void testConversionToDtoOfUserInfos() { // no profile and no group assertThat(UserInfosEntity.toDto(new UserInfosEntity(uuid, "sub_user", null, null))) .as("dto result") - .isEqualTo(new UserInfos("sub_user", null, null, null, null, null)); + .isEqualTo(new UserInfos("sub_user",null, null, null, null, null, null, null)); // with profile but without group UserProfileEntity profile = new UserProfileEntity(UUID.randomUUID(), "a profile", null, null, null, null, null, 5, 6, null, null, null); // Test mapping without quota assertThat(UserInfosEntity.toDto(new UserInfosEntity(uuid, "sub_user", profile, null))) .as("dto result") - .isEqualTo(new UserInfos("sub_user", "a profile", null, null, null, null)); + .isEqualTo(new UserInfos("sub_user", null, null, "a profile", null, null, null, null)); // Test mapping with quota assertThat(UserInfosEntity.toDtoWithDetail(new UserInfosEntity(uuid, "sub_user", profile, null), 5, 2, 6)) .as("dto result") - .isEqualTo(new UserInfos("sub_user", "a profile", 5, 2, 6, null)); + .isEqualTo(new UserInfos("sub_user", null, null, "a profile", 5, 2, 6, null)); // with profile and groups GroupInfosEntity group1 = new GroupInfosEntity(UUID.randomUUID(), "group1", Set.of()); @@ -43,11 +43,11 @@ void testConversionToDtoOfUserInfos() { // Test mapping without quota assertThat(UserInfosEntity.toDto(new UserInfosEntity(uuid, "sub_user", profile, Set.of(group1, group2)))) .as("dto result") - .isEqualTo(new UserInfos("sub_user", "a profile", null, null, null, Set.of("group1", "group2"))); + .isEqualTo(new UserInfos("sub_user", null, null, "a profile", null, null, null, Set.of("group1", "group2"))); // Test mapping with quota assertThat(UserInfosEntity.toDtoWithDetail(new UserInfosEntity(uuid, "sub_user", profile, Set.of(group1, group2)), 5, 2, 6)) .as("dto result") - .isEqualTo(new UserInfos("sub_user", "a profile", 5, 2, 6, Set.of("group1", "group2"))); + .isEqualTo(new UserInfos("sub_user", null, null, "a profile", 5, 2, 6, Set.of("group1", "group2"))); } @Test diff --git a/src/test/java/org/gridsuite/useradmin/server/NoQuotaTest.java b/src/test/java/org/gridsuite/useradmin/server/NoQuotaTest.java index 43cac99..0cbf721 100644 --- a/src/test/java/org/gridsuite/useradmin/server/NoQuotaTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/NoQuotaTest.java @@ -143,7 +143,7 @@ private UserInfos getUserInfos(String userSub) throws Exception { } private void associateProfileToUser(String userSub, String profileName) throws Exception { - UserInfos userInfos = new UserInfos(userSub, profileName, null, null, null, null); + UserInfos userInfos = new UserInfos(userSub, null, null, profileName, null, null, null, null); performPut(API_BASE_PATH + "/users/" + userSub, userInfos); } diff --git a/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java b/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java index f6b8359..b634cc7 100644 --- a/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/UserAdminTest.java @@ -219,7 +219,7 @@ void testUpdateUser() throws Exception { createGroup(GROUP_2); // udpate the user: change its name and link it to the profile and to the first group - UserInfos userInfo = new UserInfos(USER_SUB2, PROFILE_1, null, null, null, Set.of(GROUP_1)); + UserInfos userInfo = new UserInfos(USER_SUB2, null, null, PROFILE_1, null, null, null, Set.of(GROUP_1)); updateUserWithAdmin(USER_SUB, userInfo, HttpStatus.OK); // Get and check user profile @@ -233,7 +233,7 @@ void testUpdateUser() throws Exception { assertEquals(GROUP_1, userGroups.get(0).name()); // udpate the user: change groups - userInfo = new UserInfos(USER_SUB2, PROFILE_1, null, null, null, Set.of(GROUP_2)); + userInfo = new UserInfos(USER_SUB2, PROFILE_1, null, null, null, null, null, Set.of(GROUP_2)); updateUserWithAdmin(USER_SUB2, userInfo, HttpStatus.OK); // Get and check user groups @@ -244,12 +244,12 @@ void testUpdateUser() throws Exception { @Test void testUpdateUserNotFound() throws Exception { - updateUserWithAdmin("nofFound", new UserInfos("nofFound", "prof", null, null, null, null), HttpStatus.NOT_FOUND); + updateUserWithAdmin("nofFound", new UserInfos("nofFound", null, null, "prof", null, null, null, null), HttpStatus.NOT_FOUND); } @Test void testUpdateUserForbidden() throws Exception { - updateUserWithNotAdmin("dummy", new UserInfos("dummy", "prof", null, null, null, null)); + updateUserWithNotAdmin("dummy", new UserInfos("dummy", null, null, "prof", null, null, null, null)); } @Test diff --git a/src/test/java/org/gridsuite/useradmin/server/UserIdentityIntegrationTest.java b/src/test/java/org/gridsuite/useradmin/server/UserIdentityIntegrationTest.java new file mode 100644 index 0000000..279f4e1 --- /dev/null +++ b/src/test/java/org/gridsuite/useradmin/server/UserIdentityIntegrationTest.java @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.useradmin.server; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import org.gridsuite.useradmin.server.dto.UserIdentitiesResult; +import org.gridsuite.useradmin.server.dto.UserIdentity; +import org.gridsuite.useradmin.server.dto.UserInfos; +import org.gridsuite.useradmin.server.entity.UserInfosEntity; +import org.gridsuite.useradmin.server.repository.UserGroupRepository; +import org.gridsuite.useradmin.server.repository.UserInfosRepository; +import org.gridsuite.useradmin.server.repository.UserProfileRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; +import static org.gridsuite.useradmin.server.Utils.ROLES_HEADER; +import static org.gridsuite.useradmin.server.utils.TestConstants.USER_ADMIN_ROLE; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for user identity enrichment functionality. + * + * @author Achour Berrahma + */ +@AutoConfigureMockMvc +@SpringBootTest(classes = {UserAdminApplication.class}) +class UserIdentityIntegrationTest { + + private static WireMockServer wireMockServer; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private UserInfosRepository userInfosRepository; + + @Autowired + private UserGroupRepository userGroupRepository; + + @Autowired + private UserProfileRepository userProfileRepository; + + private static final String ADMIN_USER = "admin1"; + private static final String USER_SUB_1 = "user1"; + private static final String USER_SUB_2 = "user2"; + + @BeforeEach + void setUp() { + if (wireMockServer == null) { + wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); + wireMockServer.start(); + } + wireMockServer.resetAll(); + + // Inject the WireMock server URL into the service + // This is done via @DynamicPropertySource + } + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + if (wireMockServer == null) { + wireMockServer = new WireMockServer(wireMockConfig().dynamicPort()); + wireMockServer.start(); + } + registry.add("gridsuite.services.user-identity-server.base-uri", wireMockServer::baseUrl); + } + + @AfterEach + void cleanDB() { + userGroupRepository.deleteAll(); + userInfosRepository.deleteAll(); + userProfileRepository.deleteAll(); + } + + @Test + void testGetUsersWithIdentityEnrichment() throws Exception { + // Create users in database + userInfosRepository.save(new UserInfosEntity(UUID.randomUUID(), USER_SUB_1, null, null)); + userInfosRepository.save(new UserInfosEntity(UUID.randomUUID(), USER_SUB_2, null, null)); + + // Mock user-identity-server response + UserIdentity identity1 = new UserIdentity(USER_SUB_1, "John", "Doe"); + UserIdentity identity2 = new UserIdentity(USER_SUB_2, "Jane", "Smith"); + UserIdentitiesResult identitiesResult = new UserIdentitiesResult( + Map.of(USER_SUB_1, identity1, USER_SUB_2, identity2), + Map.of() + ); + + wireMockServer.stubFor(WireMock.get(urlPathEqualTo("/v1/users/identities")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(objectMapper.writeValueAsString(identitiesResult)))); + + // Call getUsers endpoint + List userInfos = objectMapper.readValue( + mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/users") + .header("userId", ADMIN_USER) + .header(ROLES_HEADER, USER_ADMIN_ROLE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + + assertEquals(2, userInfos.size()); + + // Find and verify user1 + UserInfos user1Info = userInfos.stream() + .filter(u -> USER_SUB_1.equals(u.sub())) + .findFirst() + .orElseThrow(); + assertEquals("John", user1Info.firstName()); + assertEquals("Doe", user1Info.lastName()); + + // Find and verify user2 + UserInfos user2Info = userInfos.stream() + .filter(u -> USER_SUB_2.equals(u.sub())) + .findFirst() + .orElseThrow(); + assertEquals("Jane", user2Info.firstName()); + assertEquals("Smith", user2Info.lastName()); + } + + @Test + void testGetUserWithIdentityEnrichment() throws Exception { + // Create user in database + userInfosRepository.save(new UserInfosEntity(UUID.randomUUID(), USER_SUB_1, null, null)); + + // Mock user-identity-server response for single user + UserIdentity identity = new UserIdentity(USER_SUB_1, "John", "Doe"); + + wireMockServer.stubFor(WireMock.get(urlEqualTo("/v1/users/identities/" + USER_SUB_1)) + .willReturn(aResponse() + .withStatus(200) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .withBody(objectMapper.writeValueAsString(identity)))); + + // Call getUser endpoint + UserInfos userInfo = objectMapper.readValue( + mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/users/{sub}", USER_SUB_1) + .header("userId", ADMIN_USER) + .header(ROLES_HEADER, USER_ADMIN_ROLE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + + assertEquals(USER_SUB_1, userInfo.sub()); + assertEquals("John", userInfo.firstName()); + assertEquals("Doe", userInfo.lastName()); + } + + @Test + void testGetUsersWhenIdentityServiceFails() throws Exception { + // Create user in database + userInfosRepository.save(new UserInfosEntity(UUID.randomUUID(), USER_SUB_1, null, null)); + + // Mock user-identity-server to return 500 error + wireMockServer.stubFor(WireMock.get(urlPathEqualTo("/v1/users/identities")) + .willReturn(aResponse() + .withStatus(500))); + + // Call getUsers endpoint - should still succeed but without identity info + List userInfos = objectMapper.readValue( + mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/users") + .header("userId", ADMIN_USER) + .header(ROLES_HEADER, USER_ADMIN_ROLE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + + assertEquals(1, userInfos.size()); + assertEquals(USER_SUB_1, userInfos.getFirst().sub()); + assertNull(userInfos.getFirst().firstName()); + assertNull(userInfos.getFirst().lastName()); + } + + @Test + void testGetUserWhenIdentityServiceReturns404() throws Exception { + // Create user in database + userInfosRepository.save(new UserInfosEntity(UUID.randomUUID(), USER_SUB_1, null, null)); + + // Mock user-identity-server to return 404 (user not found in identity service) + wireMockServer.stubFor(WireMock.get(urlEqualTo("/v1/users/identities/" + USER_SUB_1)) + .willReturn(aResponse() + .withStatus(404))); + + // Call getUser endpoint - should still succeed but without identity info + UserInfos userInfo = objectMapper.readValue( + mockMvc.perform(get("/" + UserAdminApi.API_VERSION + "/users/{sub}", USER_SUB_1) + .header("userId", ADMIN_USER) + .header(ROLES_HEADER, USER_ADMIN_ROLE) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference<>() { }); + + assertEquals(USER_SUB_1, userInfo.sub()); + assertNull(userInfo.firstName()); + assertNull(userInfo.lastName()); + } + +} diff --git a/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java b/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java index a4900f1..44677b3 100644 --- a/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java @@ -31,17 +31,11 @@ @SpringBootTest(classes = {UserAdminApplication.class}) class UserInfosServiceTest { - @Mock - private UserInfosService userInfosServiceSelfMock; - @Mock private DirectoryService directoryServiceMock; @Mock - private UserProfileRepository userProfileRepositoryMock; - - @Mock - private AdminRightService adminRightServiceMock; + private UserIdentityService userIdentityServiceMock; @Mock private UserAdminApplicationProps applicationPropsMock; @@ -75,10 +69,13 @@ void getUserInfoForNonExistentUser() { when(applicationPropsMock.getDefaultMaxAllowedCases()).thenReturn(20); when(applicationPropsMock.getDefaultMaxAllowedBuilds()).thenReturn(10); when(userInfosRepositoryMock.findBySub("nonExistent")).thenReturn(Optional.empty()); + when(userIdentityServiceMock.getIdentity("nonExistent")).thenReturn(Optional.empty()); UserInfos userInfos = userInfosService.getUserInfo("nonExistent"); assertNotNull(userInfos); assertEquals("nonExistent", userInfos.sub()); + assertNull(userInfos.firstName()); + assertNull(userInfos.lastName()); assertNull(userInfos.profileName()); assertEquals(20, userInfos.maxAllowedCases()); assertEquals(0, userInfos.numberCasesUsed()); From 0eb9a23d7952b1eb946d97a9a4f0e970293a09d7 Mon Sep 17 00:00:00 2001 From: achour94 Date: Mon, 22 Dec 2025 17:45:16 +0100 Subject: [PATCH 2/5] Add user identity enrichment functionality and update related DTOs Signed-off-by: achour94 --- .../gridsuite/useradmin/server/dto/UserIdentitiesResult.java | 2 +- .../gridsuite/useradmin/server/service/UserAdminService.java | 2 +- .../java/org/gridsuite/useradmin/server/DtoConverterTest.java | 2 +- .../useradmin/server/service/UserInfosServiceTest.java | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java index e51cd8a..4fc7c90 100644 --- a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java +++ b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentitiesResult.java @@ -24,4 +24,4 @@ public record UserIdentitiesResult( data = data != null ? data : Map.of(); errors = errors != null ? errors : Map.of(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java index a5bb825..b0430fe 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserAdminService.java @@ -261,4 +261,4 @@ private List enrichWithIdentities(List users) { }) .toList(); } -} \ No newline at end of file +} diff --git a/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java b/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java index dd2c18a..31cdb41 100644 --- a/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/DtoConverterTest.java @@ -24,7 +24,7 @@ void testConversionToDtoOfUserInfos() { // no profile and no group assertThat(UserInfosEntity.toDto(new UserInfosEntity(uuid, "sub_user", null, null))) .as("dto result") - .isEqualTo(new UserInfos("sub_user",null, null, null, null, null, null, null)); + .isEqualTo(new UserInfos("sub_user", null, null, null, null, null, null, null)); // with profile but without group UserProfileEntity profile = new UserProfileEntity(UUID.randomUUID(), "a profile", null, null, null, null, null, 5, 6, null, null, null); diff --git a/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java b/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java index 44677b3..bc08d7b 100644 --- a/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java +++ b/src/test/java/org/gridsuite/useradmin/server/service/UserInfosServiceTest.java @@ -12,7 +12,6 @@ import org.gridsuite.useradmin.server.entity.UserInfosEntity; import org.gridsuite.useradmin.server.entity.UserProfileEntity; import org.gridsuite.useradmin.server.repository.UserInfosRepository; -import org.gridsuite.useradmin.server.repository.UserProfileRepository; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; From f4ce8f5c087001b0c01ae8babbb9b9b7d7f97122 Mon Sep 17 00:00:00 2001 From: achour94 Date: Tue, 23 Dec 2025 10:02:13 +0100 Subject: [PATCH 3/5] Add user identity enrichment functionality and update related DTOs Signed-off-by: achour94 --- .../java/org/gridsuite/useradmin/server/dto/UserIdentity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java index 660448b..7cf4333 100644 --- a/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java +++ b/src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java @@ -15,4 +15,4 @@ public record UserIdentity( String sub, String firstName, String lastName -) { } \ No newline at end of file +) { } From 006f1a18c251d5932b366408a5bbc3d15f354da3 Mon Sep 17 00:00:00 2001 From: achour94 Date: Tue, 23 Dec 2025 10:10:49 +0100 Subject: [PATCH 4/5] Add user identity enrichment functionality and update related DTOs Signed-off-by: achour94 --- .../useradmin/server/service/UserIdentityService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java index 455aa78..fff723d 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java @@ -33,7 +33,8 @@ public class UserIdentityService { private static final Logger LOGGER = LoggerFactory.getLogger(UserIdentityService.class); private static final String USER_IDENTITY_API_VERSION = "v1"; - private static final String IDENTITIES_PATH = "/" + USER_IDENTITY_API_VERSION + "/users/identities"; + private static final String DELIMITER = "/"; + private static final String IDENTITIES_PATH = DELIMITER + USER_IDENTITY_API_VERSION + "/users/identities"; private final RestTemplate restTemplate; private final String userIdentityServerBaseUri; From a3b0f1a4bbbca1c95f0c76a9db0d4d8dd45922fc Mon Sep 17 00:00:00 2001 From: achour94 Date: Mon, 29 Dec 2025 13:27:05 +0100 Subject: [PATCH 5/5] Refactor URL construction for user identity retrieval Signed-off-by: achour94 --- .../useradmin/server/service/UserIdentityService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java index fff723d..28399d2 100644 --- a/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java +++ b/src/main/java/org/gridsuite/useradmin/server/service/UserIdentityService.java @@ -59,7 +59,9 @@ public Optional getIdentity(String sub) { } try { - String url = userIdentityServerBaseUri + IDENTITIES_PATH + "/" + sub; + String url = UriComponentsBuilder.fromUriString(userIdentityServerBaseUri + IDENTITIES_PATH) + .pathSegment(sub) + .toUriString(); UserIdentity identity = restTemplate.getForObject(url, UserIdentity.class); return Optional.ofNullable(identity); } catch (Exception e) {