Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <achour.berrahma at rte-france.com>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record UserIdentitiesResult(
Map<String, UserIdentity> data,
Map<String, Object> errors
) {
public UserIdentitiesResult {
data = data != null ? data : Map.of();
errors = errors != null ? errors : Map.of();
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/gridsuite/useradmin/server/dto/UserIdentity.java
Original file line number Diff line number Diff line change
@@ -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 <achour.berrahma at rte-france.com>
*/
public record UserIdentity(
String sub,
String firstName,
String lastName
) { }
29 changes: 28 additions & 1 deletion src/main/java/org/gridsuite/useradmin/server/dto/UserInfos.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ private UserInfos toUserInfos(Integer maxAllowedCases,
Integer maxAllowedBuilds) {
String profileName = getProfile() == null ? null : getProfile().getName();
Set<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -63,7 +67,11 @@ private UserInfos toDtoUserInfo(final UserInfosEntity entity) {
@Transactional(readOnly = true)
public List<UserInfos> getUsers() {
adminRightService.assertIsAdmin();
return userInfosRepository.findAll().stream().map(this::toDtoUserInfo).toList();
List<UserInfosEntity> entities = userInfosRepository.findAll();
List<UserInfos> users = entities.stream().map(this::toDtoUserInfo).toList();

// Enrich with identity information (firstName, lastName)
return enrichWithIdentities(users);
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -143,7 +151,9 @@ public void recordConnectionAttempt(String sub, boolean isConnectionAccepted) {
@Transactional(readOnly = true)
public Optional<UserInfos> 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)
Expand Down Expand Up @@ -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<UserIdentity> 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<UserInfos> enrichWithIdentities(List<UserInfos> users) {
if (users == null || users.isEmpty()) {
return users;
}

List<String> subs = users.stream()
.map(UserInfos::sub)
.filter(Objects::nonNull)
.toList();

Map<String, UserIdentity> identities = userIdentityService.getIdentities(subs);

return users.stream()
.map(user -> {
UserIdentity identity = identities.get(user.sub());
return identity != null ? user.withIdentity(identity) : user;
})
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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 <achour.berrahma at rte-france.com>}
*/
@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 DELIMITER = "/";
private static final String IDENTITIES_PATH = DELIMITER + 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<UserIdentity> getIdentity(String sub) {
if (sub == null || sub.isBlank()) {
return Optional.empty();
}

try {
String url = UriComponentsBuilder.fromUriString(userIdentityServerBaseUri + IDENTITIES_PATH)
.pathSegment(sub)
.toUriString();
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<String, UserIdentity> getIdentities(Collection<String> 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<UserIdentitiesResult>() { }
).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();
}
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

Expand All @@ -44,16 +48,24 @@ public UserInfos getUserInfo(String sub) {
Optional<UserInfosEntity> 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(),
Expand All @@ -64,4 +76,16 @@ private UserInfos createDefaultUserInfo(String sub, Integer casesUsed) {
private Optional<UserInfosEntity> 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<UserIdentity> identity = userIdentityService.getIdentity(userInfos.sub());
return identity.map(userInfos::withIdentity).orElse(userInfos);
}
}
4 changes: 3 additions & 1 deletion src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ spring:
gridsuite:
services:
directory-server:
base-uri: http://localhost:5026
base-uri: http://localhost:5026
user-identity-server:
base-uri: http://localhost:5034
Loading