diff options
Diffstat (limited to 'lib/src/main/java/org/onap/portalng/bff/services/KeycloakService.java')
-rw-r--r-- | lib/src/main/java/org/onap/portalng/bff/services/KeycloakService.java | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/lib/src/main/java/org/onap/portalng/bff/services/KeycloakService.java b/lib/src/main/java/org/onap/portalng/bff/services/KeycloakService.java new file mode 100644 index 0000000..b902128 --- /dev/null +++ b/lib/src/main/java/org/onap/portalng/bff/services/KeycloakService.java @@ -0,0 +1,385 @@ +/* + * + * Copyright (c) 2022. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * + */ + +package org.onap.portalng.bff.services; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.portalng.bff.exceptions.DownstreamApiProblemException; +import org.onap.portalng.bff.mappers.CredentialMapper; +import org.onap.portalng.bff.mappers.RolesMapper; +import org.onap.portalng.bff.mappers.UsersMapper; +import org.onap.portalng.bff.openapi.client_portal_keycloak.api.KeycloakApi; +import org.onap.portalng.bff.openapi.client_portal_keycloak.model.RequiredActionsKeycloakDto; +import org.onap.portalng.bff.openapi.server.model.*; +import org.onap.portalng.bff.utils.Logger; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.zalando.problem.Status; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +@Slf4j +@RequiredArgsConstructor +@Service +public class KeycloakService { + private final KeycloakApi keycloakApi; + private final ConfigurableConversionService conversionService; + private final RolesMapper rolesMapper; + private final UsersMapper usersMapper; + private final CredentialMapper credentialMapper; + + public Mono<UserResponseApiDto> createUser(CreateUserRequestApiDto request, String xRequestId) { + log.debug("Create user in keycloak. request=`{}`", request); + + final List<RoleApiDto> rolesToBeAssigned = + request.getRoles().isEmpty() ? Collections.emptyList() : request.getRoles(); + return listRoles(xRequestId) + .collectList() + .flatMap( + realmRoles -> { + final List<RoleApiDto> absentRoles = + rolesToBeAssigned.stream().filter(role -> !realmRoles.contains(role)).toList(); + if (!absentRoles.isEmpty()) { + return Mono.error( + DownstreamApiProblemException.builder() + .status(Status.NOT_FOUND) + .detail( + String.format( + "Roles not found in the realm: %s", + absentRoles.stream().map(RoleApiDto::getName).toList())) + .downstreamSystem(ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()) + .title(HttpStatus.NOT_FOUND.toString()) + .build()); + } + return Mono.just(rolesToBeAssigned); + }) + .flatMap(roles -> createUserWithRoles(request, xRequestId, roles)); + } + + private Mono<UserResponseApiDto> createUserWithRoles( + CreateUserRequestApiDto request, String xRequestId, List<RoleApiDto> roles) { + return keycloakApi + .createUserWithHttpInfo( + usersMapper.convert(request, List.of(RequiredActionsKeycloakDto.UPDATE_PASSWORD))) + .map(responseEntit -> responseEntit.getHeaders().getLocation()) + .map(URI::toString) + .map(location -> location.substring(location.lastIndexOf("/") + 1)) + .flatMap(userId -> !roles.isEmpty() ? assignRoles(userId, roles) : Mono.just(userId)) + .flatMap( + userId -> sendActionEmail(userId, List.of(RequiredActionsKeycloakDto.UPDATE_PASSWORD))) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Create user failed at sending update-password email for userName", + request.getUsername(), + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }) + .flatMap((String userId1) -> getUser(userId1, xRequestId)); + } + + public Mono<UserResponseApiDto> getUser(String userId, String xRequestId) { + log.debug("Get user from keycloak. userId=`{}`", userId); + return Mono.zip( + keycloakApi + .getUser(userId) + .map(user -> conversionService.convert(user, UserResponseApiDto.class)), + getAssignedRoles(userId, xRequestId)) + .map( + tuple -> + new UserResponseApiDto() + .username(tuple.getT1().getUsername()) + .email(tuple.getT1().getEmail()) + .enabled(tuple.getT1().getEnabled()) + .id(tuple.getT1().getId()) + .firstName(tuple.getT1().getFirstName()) + .lastName(tuple.getT1().getLastName()) + .realmRoles( + tuple.getT2().getItems().stream().map(RoleApiDto::getName).toList())) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Failed to get user", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Mono<UserListResponseApiDto> listUsers(int page, int pageSize, String xRequestId) { + log.debug("Get users from keycloak. page=`{}`, pageSize=`{}`", page, pageSize); + final int first = (page - 1) * pageSize; + + var userIdRoleMap = + listRoles(xRequestId) + .flatMap( + role -> + listUsersByRole(role.getName(), xRequestId) + .map(userResponse -> Tuples.of(role.getName(), userResponse))) + .collectList() + .map( + tupleList -> + tupleList.stream() + .collect( + Collectors.groupingBy( + tuple -> tuple.getT2().getId(), + Collectors.mapping(tuple -> tuple.getT1(), Collectors.toList())))); + + return Mono.zip( + keycloakApi.getUsersCount(null, null, null, null, null, null, null), + keycloakApi + .getUsers( + null, null, null, null, null, null, null, null, first, pageSize, null, null, + null, null) + .collectList(), + userIdRoleMap) + .map( + tuple -> { + final UserListResponseApiDto result = new UserListResponseApiDto(); + Map<String, List<String>> userRoleMap = tuple.getT3(); + result.setTotalCount(tuple.getT1()); + var roleList = + tuple.getT2().stream() + .map( + user -> + usersMapper.convert( + user, + Optional.ofNullable(userRoleMap.get(user.getId())) + .orElse(Collections.emptyList()))) + .toList(); + result.setItems(roleList); + return result; + }) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "List users failed", + null, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Mono<Void> updateUser(String userId, UpdateUserRequestApiDto request, String xRequestId) { + log.debug("Update user in keycloak. userId=`{}`, request=`{}`", userId, request); + return keycloakApi + .updateUser(userId, usersMapper.convert(request)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Failed to update user", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Mono<Void> updateUserPassword(String userId, UpdateUserPasswordRequestApiDto request) { + log.debug( + "Update password for user in keycloak. userId=`{}`, temporary=`{}`", + userId, + request.getTemporary()); + + return keycloakApi.resetUserPassword(userId, credentialMapper.convert(request)); + } + + public Mono<Void> deleteUser(String userId, String xRequestId) { + log.debug("Delete user from keycloak. userId=`{}`", userId); + + return keycloakApi + .deleteUser(userId) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Failed to delete user", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Mono<String> assignRoles(String userId, List<RoleApiDto> roles) { + log.debug( + "Assign roles to user in keycloak. userId=`{}`, roleIds=`{}`", + userId, + roles.stream().map(RoleApiDto::getId).collect(Collectors.joining(", "))); + + return keycloakApi + .addRealmRoleMappingsToUser(userId, roles.stream().map(rolesMapper::convert).toList()) + .thenReturn(userId); + } + + public Mono<RoleListResponseApiDto> updateAssignedRoles( + String userId, List<RoleApiDto> roles, String xRequestId) { + log.debug( + "Update assigned roles for user in keycloak. userId=`{}`, roleIds=`{}`", + userId, + roles.stream().map(RoleApiDto::getId).collect(Collectors.joining(", "))); + + return getAssignedRoles(userId, xRequestId) + .map(response -> response.getItems()) + .flatMap( + assignedRoles -> { + if (assignedRoles.isEmpty()) { + return Mono.empty(); + } + return unassignRoles(userId, assignedRoles); + }) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Update assigned roles failed for userId", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }) + .then( + Mono.defer( + () -> { + if (roles.isEmpty()) { + return Mono.empty(); + } + return assignRoles(userId, roles); + })) + .then(Mono.defer(() -> getAssignedRoles(userId, xRequestId))); + } + + public Mono<Void> unassignRoles(String userId, List<RoleApiDto> roles) { + log.debug( + "Unassign roles from user in keycloak. userId=`{}`, roleIds=`{}`", + userId, + roles.stream().map(RoleApiDto::getId).collect(Collectors.joining(", "))); + + return keycloakApi.deleteRealmRoleMappingsByUserId( + userId, roles.stream().map(rolesMapper::convert).toList()); + } + + public Mono<String> sendActionEmail( + String userId, java.util.List<RequiredActionsKeycloakDto> requiredActions) { + log.debug( + "Sending update actions email to user in keycloak. userId=`{}`, actions=`{}`", + userId, + requiredActions); + return keycloakApi + .executeActionsEmail(userId, null, null, null, requiredActions) + .thenReturn(userId); + } + + public Flux<RoleApiDto> listRoles(String xRequestId) { + return keycloakApi + .getRoles(null, null, null, null) + .map(role -> conversionService.convert(role, RoleApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog(xRequestId, "Get realm roles failed for ID", xRequestId, "KEYCLOAK"); + return Mono.error(ex); + }); + } + + public Mono<RoleListResponseApiDto> getAssignedRoles(String userId, String xRequestId) { + log.debug("Get assigned roles from keycloak. userId=`{}`", userId); + + return keycloakApi + .getRealmRoleMappingsByUserId(userId) + .map(role -> conversionService.convert(role, RoleApiDto.class)) + .collectList() + .map( + items -> { + final RoleListResponseApiDto result = new RoleListResponseApiDto(); + result.setTotalCount(items.size()); // keycloak does not support pagination for roles + result.setItems(items); + return result; + }) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Get assigned roles failed for userId", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Mono<RoleListResponseApiDto> getAvailableRoles(String userId, String xRequestId) { + log.debug("Get available roles from keycloak. userId=`{}`", userId); + + return keycloakApi + .getAvailableRealmRoleMappingsByUserId(userId) + .map(role -> conversionService.convert(role, RoleApiDto.class)) + .collectList() + .map( + items -> { + final RoleListResponseApiDto result = new RoleListResponseApiDto(); + result.setTotalCount(items.size()); // keycloak does not support pagination for roles + result.setItems(items); + + return result; + }) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Get available roles failed for userId", + userId, + ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()); + return Mono.error(ex); + }); + } + + public Flux<UserResponseApiDto> listUsersByRole(String roleName, String xRequestId) { + return keycloakApi + .getUsersByRole(roleName, null, null) + .log() + .map(user -> conversionService.convert(user, UserResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, "Get users by realm role failed for ID", xRequestId, "KEYCLOAK"); + return Mono.error(ex); + }); + } +} |