diff options
Diffstat (limited to 'lib')
34 files changed, 2874 insertions, 0 deletions
diff --git a/lib/LICENSE b/lib/LICENSE new file mode 100644 index 0000000..abe3069 --- /dev/null +++ b/lib/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 TNAP / development / system-team + + 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. diff --git a/lib/LICENSE_HEADER b/lib/LICENSE_HEADER new file mode 100644 index 0000000..66e028a --- /dev/null +++ b/lib/LICENSE_HEADER @@ -0,0 +1,20 @@ +/* + * + * Copyright (c) ${year}. 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 + * + * + */ diff --git a/lib/build.gradle b/lib/build.gradle new file mode 100644 index 0000000..0e2a413 --- /dev/null +++ b/lib/build.gradle @@ -0,0 +1,91 @@ +apply plugin: 'com.gorylenko.gradle-git-properties' +apply plugin: 'jacoco' +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'maven-publish' +apply plugin: 'java-library' +apply plugin: 'com.diffplug.spotless' +apply plugin: 'com.github.spotbugs' +apply plugin: 'org.sonarqube' + +group 'org.onap' +version rootProject.file('version').text.trim() + +dependencies { + implementation project(':openapi:server') + implementation project(':openapi:client-portal-prefs') + implementation project(':openapi:client-portal-history') + implementation project(':openapi:client-portal-keycloak') + + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation "org.zalando:problem:$problemVersion" + implementation "org.zalando:jackson-datatype-problem:$problemVersion" + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + implementation "org.zalando:problem-spring-webflux:$problemSpringVersion" + implementation "org.mapstruct:mapstruct:$mapStructVersion" + implementation "org.mapstruct.extensions.spring:mapstruct-spring-annotations:$mapStructExtensionsVersion" + implementation "org.mapstruct.extensions.spring:mapstruct-spring-extensions:$mapStructExtensionsVersion" + + annotationProcessor "org.mapstruct:mapstruct-processor:$mapStructVersion" + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' +} + +shadowJar { + archiveBaseName.set('portal-bff') + dependencies { + include(project(':openapi:server')) + include(project(':openapi:client-portal-history')) + include(project(':openapi:client-portal-prefs')) + include(project(':openapi:client-portal-keycloak')) + } +} + +publishing { + publications { + myLibrary(MavenPublication) { + artifactId = rootProject.name + groupId = group + version = version + artifacts = ["build/libs/portal-bff-$version-all.jar"] + pom { + name = rootProject.name + description = 'ONAP community edition of portal-bff' + } + } + } + repositories{ + mavenCentral() + } +} + +spotless { + java { + target project.fileTree(project.projectDir) { + include '**/*.java' + exclude '**/build/**' + } + removeUnusedImports() + trimTrailingWhitespace() + googleJavaFormat('1.15.0') + } +} + +spotbugs { + ignoreFailures = false + effort = "max" + reportLevel = "high" + excludeFilter = file("$rootProject.projectDir/spotbugs-exclude.xml") +} + +sonarqube { + properties { + property "sonar.projectKey", "tnap.SONAR.portal.portal-bff-ce" + property "sonar.projectName", "portal-bff-ce" + property "sonar.projectDescription", "Community edition of the the ONAP portal" + property "sonar.exclusions", "**/build**" + } +}
\ No newline at end of file diff --git a/lib/src/main/java/org/onap/portal/bff/config/BeansConfig.java b/lib/src/main/java/org/onap/portal/bff/config/BeansConfig.java new file mode 100644 index 0000000..a0d0555 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/BeansConfig.java @@ -0,0 +1,191 @@ +/* + * + * 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.portal.bff.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.vavr.jackson.datatype.VavrModule; +import java.time.Clock; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.utils.Logger; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.zalando.problem.jackson.ProblemModule; +import reactor.core.publisher.Mono; + +@Slf4j +@Configuration +public class BeansConfig { + + public static final String OAUTH2_EXCHANGE_FILTER_FUNCTION = "oauth2ExchangeFilterFunction"; + private static final String ID_TOKEN_EXCHANGE_FILTER_FUNCTION = "idTokenExchangeFilterFunction"; + private static final String ERROR_HANDLING_EXCHANGE_FILTER_FUNCTION = + "errorHandlingExchangeFilterFunction"; + private static final String LOG_REQUEST_EXCHANGE_FILTER_FUNCTION = + "logRequestExchangeFilterFunction"; + private static final String LOG_RESPONSE_EXCHANGE_FILTER_FUNCTION = + "logResponseExchangeFilterFunction"; + private static final String CLIENT_REGISTRATION_ID = "keycloak"; + public static final String X_REQUEST_ID = "X-Request-Id"; + + @Bean(name = OAUTH2_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction oauth2ExchangeFilterFunction( + ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + final ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Filter = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Filter.setDefaultClientRegistrationId(CLIENT_REGISTRATION_ID); + + return oauth2Filter; + } + + @Bean(name = ID_TOKEN_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction idTokenExchangeFilterFunction() { + return new IdTokenExchangeFilterFunction(); + } + + @Bean(name = ERROR_HANDLING_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction errorHandlingExchangeFilterFunction() { + return ExchangeFilterFunction.ofResponseProcessor( + clientResponse -> { + if (clientResponse.statusCode().isError()) { + return clientResponse + .bodyToMono(String.class) + .doOnNext(s -> log.error("Received error response from downstream: {}", s)) + .flatMap( + downstreamExceptionBody -> { + try { + return Mono.error( + new ObjectMapper() + .readValue( + downstreamExceptionBody, DownstreamApiProblemException.class)); + } catch (JsonProcessingException e) { + return Mono.error(DownstreamApiProblemException.builder().build()); + } + }); + } + return Mono.just(clientResponse); + }); + } + + // + // Don't use this. Log will is written in the LoggerInterceptor + // + @Bean(name = LOG_REQUEST_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction logRequestExchangeFilterFunction() { + return ExchangeFilterFunction.ofRequestProcessor( + clientRequest -> { + List<String> xRequestIdList = clientRequest.headers().get(X_REQUEST_ID); + if (xRequestIdList != null && !xRequestIdList.isEmpty()) { + String xRequestId = xRequestIdList.get(0); + Logger.requestLog(xRequestId, clientRequest.method(), clientRequest.url()); + } + return Mono.just(clientRequest); + }); + } + + @Bean(name = LOG_RESPONSE_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction logResponseExchangeFilterFunction() { + return ExchangeFilterFunction.ofResponseProcessor( + clientResponse -> { + String xRequestId = "not set"; + List<String> xRequestIdList = clientResponse.headers().header(X_REQUEST_ID); + if (xRequestIdList != null && !xRequestIdList.isEmpty()) + xRequestId = xRequestIdList.get(0); + Logger.responseLog(xRequestId, clientResponse.statusCode()); + return Mono.just(clientResponse); + }); + } + + @Bean + ExchangeStrategies exchangeStrategies(ObjectMapper objectMapper) { + return ExchangeStrategies.builder() + .codecs( + configurer -> { + final ClientCodecConfigurer.ClientDefaultCodecs defaultCodecs = + configurer.defaultCodecs(); + + defaultCodecs.maxInMemorySize(16 * 1024 * 1024); // 16MB + defaultCodecs.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper)); + defaultCodecs.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper)); + }) + .build(); + } + + // we need to use prototype scope to always create new instance of the bean + // because internally WebClient.Builder is mutable + @Bean + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + WebClient.Builder webClientBuilder( + ExchangeStrategies exchangeStrategies, + @Qualifier(ID_TOKEN_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction idTokenExchangeFilterFunction, + @Qualifier(ERROR_HANDLING_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction errorHandlingExchangeFilterFunction, + @Qualifier(LOG_RESPONSE_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction logResponseExchangeFilterFunction) { + return WebClient.builder() + .exchangeStrategies(exchangeStrategies) + .filter(idTokenExchangeFilterFunction) + .filter(errorHandlingExchangeFilterFunction) + .filter(logResponseExchangeFilterFunction); + } + + @Bean + Clock clock() { + return Clock.systemUTC(); + } + + @Bean + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + return builder + .modules(new VavrModule(), new ProblemModule(), new JavaTimeModule()) + .build() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @Bean + public XmlMapper xmlMapper() { + return XmlMapper.builder() + .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .build(); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/ConversionServiceConfig.java b/lib/src/main/java/org/onap/portal/bff/config/ConversionServiceConfig.java new file mode 100644 index 0000000..09a8d53 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/ConversionServiceConfig.java @@ -0,0 +1,59 @@ +/* + * + * 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.portal.bff.config; + +import io.vavr.collection.List; +import org.onap.portal.bff.mappers.ActionsMapper; +import org.onap.portal.bff.mappers.PreferencesMapper; +import org.onap.portal.bff.mappers.RolesMapper; +import org.onap.portal.bff.mappers.UsersMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.core.convert.support.DefaultConversionService; + +@SuppressWarnings("rawtypes") +@Configuration +public class ConversionServiceConfig { + + @Bean + public ConfigurableConversionService conversionService( + ActionsMapper actionsMapper, + PreferencesMapper preferencesMapper, + RolesMapper rolesMapper, + UsersMapper usersMapper) { + final List<Converter> converters = + List.of( + actionsMapper, + preferencesMapper, + preferencesMapper, + actionsMapper, + rolesMapper, + usersMapper); + + final ConfigurableConversionService conversionService = new DefaultConversionService(); + converters.forEach(conversionService::addConverter); + + return conversionService; + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/IdTokenExchangeFilterFunction.java b/lib/src/main/java/org/onap/portal/bff/config/IdTokenExchangeFilterFunction.java new file mode 100644 index 0000000..be3493d --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/IdTokenExchangeFilterFunction.java @@ -0,0 +1,125 @@ +/* + * + * 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.portal.bff.config; + +import com.nimbusds.jwt.JWTParser; +import io.vavr.control.Option; +import io.vavr.control.Try; +import java.util.List; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.server.ServerWebExchange; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import reactor.core.publisher.Mono; + +public class IdTokenExchangeFilterFunction implements ExchangeFilterFunction { + + public static final String X_AUTH_IDENTITY_HEADER = "X-Auth-Identity"; + public static final String CLAIM_NAME_ROLES = "roles"; + + private static final List<String> EXCLUDED_PATHS_PATTERNS = + List.of( + "/actuator/**", "**/actuator/**", "*/actuator/**", "/**/actuator/**", "/*/actuator/**"); + + private static final Mono<ServerWebExchange> serverWebExchangeFromContext = + Mono.deferContextual(Mono::just) + .filter(context -> context.hasKey(ServerWebExchange.class)) + .map(context -> context.get(ServerWebExchange.class)); + + @Override + public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { + boolean shouldNotFilter = + EXCLUDED_PATHS_PATTERNS.stream() + .anyMatch( + excludedPath -> + new AntPathMatcher().match(excludedPath, request.url().getRawPath())); + if (shouldNotFilter) { + return next.exchange(request).switchIfEmpty(Mono.defer(() -> next.exchange(request))); + } + return extractServerWebExchange(request) + .flatMap(IdTokenExchangeFilterFunction::extractIdentityHeader) + .flatMap( + idToken -> { + final ClientRequest requestWithIdToken = + ClientRequest.from(request).header(X_AUTH_IDENTITY_HEADER, idToken).build(); + + return next.exchange(requestWithIdToken); + }) + .switchIfEmpty(Mono.defer(() -> next.exchange(request))); + } + + private Mono<ServerWebExchange> extractServerWebExchange(ClientRequest request) { + return Mono.justOrEmpty(request.attribute(ServerWebExchange.class.getName())) + .cast(ServerWebExchange.class) + .switchIfEmpty(serverWebExchangeFromContext); + } + + private static Mono<String> extractIdentityHeader(ServerWebExchange exchange) { + return io.vavr.collection.List.ofAll( + exchange.getRequest().getHeaders().getOrEmpty(X_AUTH_IDENTITY_HEADER)) + .headOption() + .map(Mono::just) + .getOrElse(Mono.error(Problem.valueOf(Status.FORBIDDEN, "ID token is missing"))); + } + + private static Mono<String> extractIdToken(ServerWebExchange exchange) { + return extractIdentityHeader(exchange) + .map(identityHeader -> identityHeader.replace("Bearer ", "")); + } + + public static Mono<Void> validateAccess( + ServerWebExchange exchange, List<String> rolesListForMethod) { + + return extractRoles(exchange) + .map(roles -> roles.stream().anyMatch(rolesListForMethod::contains)) + .flatMap( + match -> { + if (Boolean.TRUE.equals(match)) { + return Mono.empty(); + } else { + return Mono.error(Problem.valueOf(Status.FORBIDDEN)); + } + }); + } + + private static Mono<List<String>> extractRoles(ServerWebExchange exchange) { + return extractIdToken(exchange) + .flatMap( + token -> + Try.of(() -> JWTParser.parse(token)) + .mapTry(jwt -> Option.of(jwt.getJWTClaimsSet())) + .map( + optionJwtClaimSet -> + optionJwtClaimSet + .flatMap( + jwtClaimSet -> + Option.of(jwtClaimSet.getClaim(CLAIM_NAME_ROLES))) + .map(obj -> (List<String>) obj)) + .map(Mono::just) + .getOrElseGet(Mono::error)) + .map(optionRoles -> optionRoles.getOrElse(List.of())); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/LoggerInterceptor.java b/lib/src/main/java/org/onap/portal/bff/config/LoggerInterceptor.java new file mode 100644 index 0000000..4fa2d82 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/LoggerInterceptor.java @@ -0,0 +1,52 @@ +/* + * + * 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.portal.bff.config; + +import java.util.List; +import org.onap.portal.bff.utils.Logger; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +@Component +public class LoggerInterceptor extends ServerWebExchangeContextFilter { + public static final String EXCHANGE_CONTEXT_ATTRIBUTE = + ServerWebExchangeContextFilter.class.getName() + ".EXCHANGE_CONTEXT"; + + public static final String X_REQUEST_ID = "X-Request-Id"; + + @Override + public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { + List<String> xRequestIdList = exchange.getRequest().getHeaders().get(X_REQUEST_ID); + if (xRequestIdList != null && !xRequestIdList.isEmpty()) { + String xRequestId = xRequestIdList.get(0); + Logger.requestLog( + xRequestId, exchange.getRequest().getMethod(), exchange.getRequest().getURI()); + exchange.getResponse().getHeaders().add(X_REQUEST_ID, xRequestId); + } + return chain + .filter(exchange) + .contextWrite(cxt -> cxt.put(EXCHANGE_CONTEXT_ATTRIBUTE, exchange)); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/MapperSpringConfig.java b/lib/src/main/java/org/onap/portal/bff/config/MapperSpringConfig.java new file mode 100644 index 0000000..c7e3711 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/MapperSpringConfig.java @@ -0,0 +1,28 @@ +/* + * + * 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.portal.bff.config; + +import org.mapstruct.MapperConfig; +import org.mapstruct.extensions.spring.converter.ConversionServiceAdapterGenerator; + +@MapperConfig(componentModel = "spring", uses = ConversionServiceAdapterGenerator.class) +public interface MapperSpringConfig {} diff --git a/lib/src/main/java/org/onap/portal/bff/config/PortalBffConfig.java b/lib/src/main/java/org/onap/portal/bff/config/PortalBffConfig.java new file mode 100644 index 0000000..42454c8 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/PortalBffConfig.java @@ -0,0 +1,63 @@ +/* + * + * 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.portal.bff.config; + +import io.vavr.control.Option; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import reactor.core.publisher.Mono; + +/** + * Class that contains configuration of the downstream apis. This could be username and password or + * urls. + */ +@Valid +@ConstructorBinding +@ConfigurationProperties("portal-bff") +@Data +public class PortalBffConfig { + + @NotBlank private final String realm; + @NotBlank private final String portalServiceUrl; + @NotBlank private final String portalPrefsUrl; + @NotBlank private final String portalHistoryUrl; + @NotBlank private final String keycloakUrl; + + @NotNull private final Map<String, List<String>> accessControl; + + public Mono<List<String>> getRoles(String method) { + return Option.of(accessControl.get(method)) + .map(Mono::just) + .getOrElse( + Mono.error( + Problem.valueOf( + Status.FORBIDDEN, "The user does not have the necessary access rights"))); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/SecurityConfig.java b/lib/src/main/java/org/onap/portal/bff/config/SecurityConfig.java new file mode 100644 index 0000000..0d33980 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/SecurityConfig.java @@ -0,0 +1,77 @@ +/* + * + * 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.portal.bff.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@EnableWebFluxSecurity +@Configuration +public class SecurityConfig { + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + return http.httpBasic() + .disable() + .formLogin() + .disable() + .csrf() + .disable() + .cors() + .and() + .authorizeExchange() + .pathMatchers(HttpMethod.GET, "/api-docs.html", "/api.yaml", "/webjars/**", "/actuator/**") + .permitAll() + .anyExchange() + .authenticated() + .and() + .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt) + .oauth2Client() + .and() + .build(); + } + + @Bean + ReactiveOAuth2AuthorizedClientManager reactiveOAuth2AuthorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials().build(); + + final DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/clients/AbstractClientConfig.java b/lib/src/main/java/org/onap/portal/bff/config/clients/AbstractClientConfig.java new file mode 100644 index 0000000..85ee8ba --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/clients/AbstractClientConfig.java @@ -0,0 +1,87 @@ +/* + * + * 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.portal.bff.config.clients; + +import java.time.Duration; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractClientConfig<E> { + private final Class<E> errorResponseTypeClass; + + protected ExchangeFilterFunction errorHandlingExchangeFilterFunction() { + return ExchangeFilterFunction.ofResponseProcessor( + clientResponse -> { + if (clientResponse.statusCode().isError()) { + return clientResponse + .bodyToMono(errorResponseTypeClass) + .doOnNext(s -> log.error("Received error response from downstream: {}", s)) + .flatMap( + problemResponse -> + Mono.error(mapException(problemResponse, clientResponse.statusCode()))); + } + return Mono.just(clientResponse); + }); + } + + protected abstract DownstreamApiProblemException mapException( + E errorResponse, HttpStatus httpStatus); + + protected ClientHttpConnector getClientHttpConnector() { + // ConnectionTimeouts introduced due to + // io.netty.channel.unix.Errors$NativeIoException: readAddress(..) failed: Connection reset by + // peer issue + // https://github.com/reactor/reactor-netty/issues/1774#issuecomment-908066283 + ConnectionProvider connectionProvider = + ConnectionProvider.builder("fixed") + .maxConnections(500) + .maxIdleTime(Duration.ofSeconds(20)) + .maxLifeTime(Duration.ofSeconds(60)) + .pendingAcquireTimeout(Duration.ofSeconds(60)) + .evictInBackground(Duration.ofSeconds(120)) + .build(); + return new ReactorClientHttpConnector(HttpClient.create(connectionProvider)); + } + + protected WebClient getWebClient( + WebClient.Builder webClientBuilder, List<ExchangeFilterFunction> filters) { + if (filters != null) { + filters.forEach(webClientBuilder::filter); + } + return webClientBuilder + .filter(errorHandlingExchangeFilterFunction()) + .clientConnector(getClientHttpConnector()) + .build(); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/clients/KeycloakConfig.java b/lib/src/main/java/org/onap/portal/bff/config/clients/KeycloakConfig.java new file mode 100644 index 0000000..0935a00 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/clients/KeycloakConfig.java @@ -0,0 +1,104 @@ +/* + * + * 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.portal.bff.config.clients; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.config.BeansConfig; +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.client_portal_keycloak.ApiClient; +import org.onap.portal.bff.openapi.client_portal_keycloak.api.KeycloakApi; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.ErrorResponseKeycloakDto; +import org.onap.portal.bff.openapi.server.model.ProblemApiDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Configuration +public class KeycloakConfig extends AbstractClientConfig<ErrorResponseKeycloakDto> { + private final ObjectMapper objectMapper; + private final PortalBffConfig bffConfig; + private final ExchangeFilterFunction oauth2ExchangeFilterFunction; + + @Autowired + public KeycloakConfig( + @Qualifier(BeansConfig.OAUTH2_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction oauth2ExchangeFilterFunction, + ObjectMapper objectMapper, + PortalBffConfig bffConfig) { + super(ErrorResponseKeycloakDto.class); + this.objectMapper = objectMapper; + this.bffConfig = bffConfig; + this.oauth2ExchangeFilterFunction = oauth2ExchangeFilterFunction; + } + + @Bean + public KeycloakApi keycloakApi(WebClient.Builder webClientBuilder) { + return constructApiClient(webClientBuilder, KeycloakApi::new); + } + + private <T> T constructApiClient( + WebClient.Builder webClientBuilder, Function<ApiClient, T> apiConstructor) { + final ApiClient apiClient = + new ApiClient( + getWebClient(webClientBuilder, List.of(oauth2ExchangeFilterFunction)), + objectMapper, + objectMapper.getDateFormat()); + + // Extract service name and version from BasePath + String urlBasePathPrefix = + String.format("%s/auth/admin/realms/%s", bffConfig.getKeycloakUrl(), bffConfig.getRealm()); + + return apiConstructor.apply(apiClient.setBasePath(urlBasePathPrefix)); + } + + @Override + protected DownstreamApiProblemException mapException( + ErrorResponseKeycloakDto errorResponse, HttpStatus httpStatus) { + String errorDetail = + errorResponse.getErrorMessage() != null + ? errorResponse.getErrorMessage() + : errorResponse.getError(); + + return DownstreamApiProblemException.builder() + .title(httpStatus.toString()) + .detail(errorDetail) + .downstreamSystem(ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()) + .downstreamMessageId("not set by downstream system") + .downstreamStatus(httpStatus.value()) + .build(); + } + + @Override + protected ClientHttpConnector getClientHttpConnector() { + return null; + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/clients/PortalHistoryConfig.java b/lib/src/main/java/org/onap/portal/bff/config/clients/PortalHistoryConfig.java new file mode 100644 index 0000000..b71608f --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/clients/PortalHistoryConfig.java @@ -0,0 +1,97 @@ +/* + * + * 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.portal.bff.config.clients; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.config.BeansConfig; +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.client_portal_history.ApiClient; +import org.onap.portal.bff.openapi.client_portal_history.api.ActionsApi; +import org.onap.portal.bff.openapi.client_portal_history.model.ProblemPortalHistoryDto; +import org.onap.portal.bff.openapi.server.model.ProblemApiDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Configuration +public class PortalHistoryConfig extends AbstractClientConfig<ProblemPortalHistoryDto> { + private final ObjectMapper objectMapper; + private final PortalBffConfig bffConfig; + private final ExchangeFilterFunction oauth2ExchangeFilterFunction; + + @Autowired + public PortalHistoryConfig( + @Qualifier(BeansConfig.OAUTH2_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction oauth2ExchangeFilterFunction, + ObjectMapper objectMapper, + PortalBffConfig bffConfig) { + super(ProblemPortalHistoryDto.class); + this.objectMapper = objectMapper; + this.bffConfig = bffConfig; + this.oauth2ExchangeFilterFunction = oauth2ExchangeFilterFunction; + } + + @Bean + public ActionsApi portalHistoryActionApi(WebClient.Builder webClientBuilder) { + return constructApiClient(webClientBuilder, ActionsApi::new); + } + + private <T> T constructApiClient( + WebClient.Builder webClientBuilder, Function<ApiClient, T> apiConstructor) { + final ApiClient apiClient = + new ApiClient( + getWebClient(webClientBuilder, List.of(oauth2ExchangeFilterFunction)), + objectMapper, + objectMapper.getDateFormat()); + final String generatedBasePath = apiClient.getBasePath(); + String basePath = ""; + try { + basePath = bffConfig.getPortalHistoryUrl() + new URL(generatedBasePath).getPath(); + } catch (MalformedURLException e) { + log.error(e.getLocalizedMessage()); + } + return apiConstructor.apply(apiClient.setBasePath(basePath)); + } + + @Override + protected DownstreamApiProblemException mapException( + ProblemPortalHistoryDto errorResponse, HttpStatus httpStatus) { + return DownstreamApiProblemException.builder() + .title(httpStatus.toString()) + .detail(errorResponse.getDetail()) + .downstreamMessageId(errorResponse.getType()) + .downstreamSystem(ProblemApiDto.DownstreamSystemEnum.PORTAL_HISTORY.toString()) + .downstreamStatus(httpStatus.value()) + .build(); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/config/clients/PortalPrefsConfig.java b/lib/src/main/java/org/onap/portal/bff/config/clients/PortalPrefsConfig.java new file mode 100644 index 0000000..5e23348 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/config/clients/PortalPrefsConfig.java @@ -0,0 +1,96 @@ +/* + * + * 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.portal.bff.config.clients; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.config.BeansConfig; +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.client_portal_prefs.ApiClient; +import org.onap.portal.bff.openapi.client_portal_prefs.api.PreferencesApi; +import org.onap.portal.bff.openapi.client_portal_prefs.model.ProblemPortalPrefsDto; +import org.onap.portal.bff.openapi.server.model.ProblemApiDto; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +@Slf4j +@Configuration +public class PortalPrefsConfig extends AbstractClientConfig<ProblemPortalPrefsDto> { + private final ObjectMapper objectMapper; + private final PortalBffConfig bffConfig; + private final ExchangeFilterFunction oauth2ExchangeFilterFunction; + + public PortalPrefsConfig( + @Qualifier(BeansConfig.OAUTH2_EXCHANGE_FILTER_FUNCTION) + ExchangeFilterFunction oauth2ExchangeFilterFunction, + ObjectMapper objectMapper, + PortalBffConfig bffConfig) { + super(ProblemPortalPrefsDto.class); + this.objectMapper = objectMapper; + this.bffConfig = bffConfig; + this.oauth2ExchangeFilterFunction = oauth2ExchangeFilterFunction; + } + + @Bean + public PreferencesApi portalPrefsApi(WebClient.Builder webClientBuilder) { + return constructApiClient(webClientBuilder, PreferencesApi::new); + } + + private <T> T constructApiClient( + WebClient.Builder webClientBuilder, Function<ApiClient, T> apiConstructor) { + final ApiClient apiClient = + new ApiClient( + getWebClient(webClientBuilder, List.of(oauth2ExchangeFilterFunction)), + objectMapper, + objectMapper.getDateFormat()); + + final String generatedBasePath = apiClient.getBasePath(); + String basePath = ""; + try { + basePath = bffConfig.getPortalPrefsUrl() + new URL(generatedBasePath).getPath(); + } catch (MalformedURLException e) { + log.error(e.getLocalizedMessage()); + } + return apiConstructor.apply(apiClient.setBasePath(basePath)); + } + + @Override + protected DownstreamApiProblemException mapException( + ProblemPortalPrefsDto errorResponse, HttpStatus httpStatus) { + return DownstreamApiProblemException.builder() + .title(httpStatus.toString()) + .detail(errorResponse.getDetail()) + .downstreamMessageId(errorResponse.getType()) + .downstreamSystem(ProblemApiDto.DownstreamSystemEnum.PORTAL_PREFS.toString()) + .downstreamStatus(httpStatus.value()) + .build(); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/AbstractBffController.java b/lib/src/main/java/org/onap/portal/bff/controller/AbstractBffController.java new file mode 100644 index 0000000..bc92b68 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/AbstractBffController.java @@ -0,0 +1,46 @@ +/* + * + * 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.portal.bff.controller; + +import org.onap.portal.bff.config.IdTokenExchangeFilterFunction; +import org.onap.portal.bff.config.PortalBffConfig; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +public abstract class AbstractBffController { + + protected PortalBffConfig bffConfig; + + protected AbstractBffController(PortalBffConfig bffConfig) { + this.bffConfig = bffConfig; + } + + public Mono<Void> checkRoleAccess(String method, ServerWebExchange exchange) { + return bffConfig + .getRoles(method) + .flatMap( + roles -> + roles.contains("*") + ? Mono.empty() + : IdTokenExchangeFilterFunction.validateAccess(exchange, roles)); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/ActionsController.java b/lib/src/main/java/org/onap/portal/bff/controller/ActionsController.java new file mode 100644 index 0000000..ece6683 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/ActionsController.java @@ -0,0 +1,84 @@ +/* + * + * 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.portal.bff.controller; + +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.openapi.server.api.ActionsApi; +import org.onap.portal.bff.openapi.server.model.ActionsListResponseApiDto; +import org.onap.portal.bff.openapi.server.model.ActionsResponseApiDto; +import org.onap.portal.bff.openapi.server.model.CreateActionRequestApiDto; +import org.onap.portal.bff.services.ActionService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +public class ActionsController extends AbstractBffController implements ActionsApi { + public static final String CREATE = "ACTIONS_CREATE"; + public static final String GET = "ACTIONS_GET"; + public static final String LIST = "ACTIONS_LIST"; + + private final ActionService actionService; + + public ActionsController(PortalBffConfig bffConfig, ActionService actionService) { + super(bffConfig); + this.actionService = actionService; + } + + @Override + public Mono<ResponseEntity<ActionsResponseApiDto>> createAction( + String userId, + String xRequestId, + Mono<CreateActionRequestApiDto> createActionRequestApiDto, + ServerWebExchange exchange) { + return checkRoleAccess(CREATE, exchange) + .then(createActionRequestApiDto) + .flatMap(action -> actionService.createAction(userId, xRequestId, action)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<ActionsListResponseApiDto>> getActions( + String userId, + Integer page, + Integer pageSize, + Integer showLastHours, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(GET, exchange) + .then(actionService.getActions(userId, xRequestId, page, pageSize, showLastHours)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<ActionsListResponseApiDto>> listActions( + Integer page, + Integer pageSize, + Integer showLastHours, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(LIST, exchange) + .then(actionService.listActions(xRequestId, page, pageSize, showLastHours)) + .map(ResponseEntity::ok); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/BffControllerAdvice.java b/lib/src/main/java/org/onap/portal/bff/controller/BffControllerAdvice.java new file mode 100644 index 0000000..3580495 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/BffControllerAdvice.java @@ -0,0 +1,28 @@ +/* + * + * 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.portal.bff.controller; + +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.zalando.problem.spring.webflux.advice.ProblemHandling; + +@RestControllerAdvice +public class BffControllerAdvice implements ProblemHandling {} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/PreferencesController.java b/lib/src/main/java/org/onap/portal/bff/controller/PreferencesController.java new file mode 100644 index 0000000..625d034 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/PreferencesController.java @@ -0,0 +1,77 @@ +/* + * + * 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.portal.bff.controller; + +import javax.validation.Valid; +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.openapi.server.api.PreferencesApi; +import org.onap.portal.bff.openapi.server.model.CreatePreferencesRequestApiDto; +import org.onap.portal.bff.openapi.server.model.PreferencesResponseApiDto; +import org.onap.portal.bff.services.PreferencesService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +public class PreferencesController extends AbstractBffController implements PreferencesApi { + public static final String CREATE = "PREFERENCES_CREATE"; + public static final String GET = "PREFERENCES_GET"; + public static final String UPDATE = "PREFERENCES_UPDATE"; + + private final PreferencesService preferencesService; + + public PreferencesController(PortalBffConfig bffConfig, PreferencesService preferencesService) { + super(bffConfig); + this.preferencesService = preferencesService; + } + + @Override + public Mono<ResponseEntity<PreferencesResponseApiDto>> getPreferences( + String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(GET, exchange) + .then(preferencesService.getPreferences(xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<PreferencesResponseApiDto>> savePreferences( + @Valid Mono<CreatePreferencesRequestApiDto> preferencesApiDto, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(CREATE, exchange) + .then(preferencesApiDto) + .flatMap(request -> preferencesService.createPreferences(xRequestId, request)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<PreferencesResponseApiDto>> updatePreferences( + @Valid Mono<CreatePreferencesRequestApiDto> preferencesApiDto, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(UPDATE, exchange) + .then(preferencesApiDto) + .flatMap(request -> preferencesService.updatePreferences(xRequestId, request)) + .map(ResponseEntity::ok); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/RolesController.java b/lib/src/main/java/org/onap/portal/bff/controller/RolesController.java new file mode 100644 index 0000000..34d495f --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/RolesController.java @@ -0,0 +1,56 @@ +/* + * + * 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.portal.bff.controller; + +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.openapi.server.api.RolesApi; +import org.onap.portal.bff.openapi.server.model.RoleListResponseApiDto; +import org.onap.portal.bff.services.KeycloakService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@RestController +public class RolesController extends AbstractBffController implements RolesApi { + + public static final String LIST = "ROLE_LIST"; + + private final KeycloakService keycloakService; + + @Autowired + public RolesController(PortalBffConfig bffConfig, KeycloakService keycloakService) { + super(bffConfig); + this.keycloakService = keycloakService; + } + + @Override + public Mono<ResponseEntity<RoleListResponseApiDto>> listRoles( + String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(LIST, exchange) + .thenMany(keycloakService.listRoles(xRequestId)) + .collectList() + .map(roles -> new RoleListResponseApiDto().items(roles).totalCount(roles.size())) + .map(ResponseEntity::ok); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/controller/UsersController.java b/lib/src/main/java/org/onap/portal/bff/controller/UsersController.java new file mode 100644 index 0000000..f67809b --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/controller/UsersController.java @@ -0,0 +1,145 @@ +/* + * + * 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.portal.bff.controller; + +import io.vavr.collection.List; +import org.onap.portal.bff.config.PortalBffConfig; +import org.onap.portal.bff.openapi.server.api.UsersApi; +import org.onap.portal.bff.openapi.server.model.CreateUserRequestApiDto; +import org.onap.portal.bff.openapi.server.model.RoleApiDto; +import org.onap.portal.bff.openapi.server.model.RoleListResponseApiDto; +import org.onap.portal.bff.openapi.server.model.UpdateUserPasswordRequestApiDto; +import org.onap.portal.bff.openapi.server.model.UpdateUserRequestApiDto; +import org.onap.portal.bff.openapi.server.model.UserListResponseApiDto; +import org.onap.portal.bff.openapi.server.model.UserResponseApiDto; +import org.onap.portal.bff.services.KeycloakService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +public class UsersController extends AbstractBffController implements UsersApi { + + public static final String CREATE = "USER_CREATE"; + public static final String GET = "USER_GET"; + public static final String UPDATE = "USER_UPDATE"; + public static final String DELETE = "USER_DELETE"; + public static final String LIST = "USER_LIST"; + public static final String UPDATE_PASSWORD = "USER_UPDATE_PASSWORD"; + public static final String UPDATE_ROLES = "USER_UPDATE_ROLES"; + public static final String LIST_ROLES = "USER_LIST_ROLES"; + public static final String LIST_AVAILABLE_ROLES = "USER_LIST_AVAILABLE_ROLES"; + + private final KeycloakService keycloakService; + + @Autowired + public UsersController(PortalBffConfig bffConfig, KeycloakService keycloakService) { + super(bffConfig); + this.keycloakService = keycloakService; + } + + @Override + public Mono<ResponseEntity<UserResponseApiDto>> createUser( + Mono<CreateUserRequestApiDto> requestMono, String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(CREATE, exchange) + .then(requestMono.flatMap(request -> keycloakService.createUser(request, xRequestId))) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<UserResponseApiDto>> getUser( + String userId, String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(GET, exchange) + .then(keycloakService.getUser(userId, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<Void>> updateUser( + String userId, + Mono<UpdateUserRequestApiDto> requestMono, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(UPDATE, exchange) + .then(requestMono) + .flatMap(request -> keycloakService.updateUser(userId, request, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<Void>> deleteUser( + String userId, String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(DELETE, exchange) + .then(keycloakService.deleteUser(userId, xRequestId)) + .thenReturn(ResponseEntity.noContent().build()); + } + + @Override + public Mono<ResponseEntity<UserListResponseApiDto>> listUsers( + Integer page, Integer pageSize, String xRequestId, ServerWebExchange exchange) { + + return checkRoleAccess(LIST, exchange) + .then(keycloakService.listUsers(page, pageSize, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<Void>> updatePassword( + String userId, + Mono<UpdateUserPasswordRequestApiDto> requestMono, + String xRequestId, + ServerWebExchange exchange) { + return checkRoleAccess(UPDATE_PASSWORD, exchange) + .then(requestMono) + .flatMap(request -> keycloakService.updateUserPassword(userId, request)) + .thenReturn(ResponseEntity.noContent().build()); + } + + @Override + public Mono<ResponseEntity<RoleListResponseApiDto>> listAvailableRoles( + String userId, String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(LIST_AVAILABLE_ROLES, exchange) + .then(keycloakService.getAvailableRoles(userId, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<RoleListResponseApiDto>> listAssignedRoles( + String userId, String xRequestId, ServerWebExchange exchange) { + return checkRoleAccess(LIST_ROLES, exchange) + .then(keycloakService.getAssignedRoles(userId, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono<ResponseEntity<RoleListResponseApiDto>> updateAssignedRoles( + String userId, String xRequestId, Flux<RoleApiDto> rolesFlux, ServerWebExchange exchange) { + return checkRoleAccess(UPDATE_ROLES, exchange) + .then(rolesFlux.collectList()) + .map(List::ofAll) + .flatMap(roles -> keycloakService.updateAssignedRoles(userId, roles, xRequestId)) + .map(ResponseEntity::ok); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/exceptions/DownstreamApiProblemException.java b/lib/src/main/java/org/onap/portal/bff/exceptions/DownstreamApiProblemException.java new file mode 100644 index 0000000..35b895e --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/exceptions/DownstreamApiProblemException.java @@ -0,0 +1,65 @@ +/* + * + * 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.portal.bff.exceptions; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.net.URI; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.onap.portal.bff.openapi.server.model.ConstraintViolationApiDto; +import org.zalando.problem.AbstractThrowableProblem; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import org.zalando.problem.StatusType; + +/** The default portal-bff exception */ +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +@ToString +@JsonIgnoreProperties +public class DownstreamApiProblemException extends AbstractThrowableProblem { + + @Builder.Default private final URI type = Problem.DEFAULT_TYPE; + @Builder.Default private final String title = "Bad gateway error"; + + @JsonIgnore @Builder.Default private final transient StatusType status = Status.BAD_GATEWAY; + + @Builder.Default + private final String detail = "Please find more detail under correlationId: 'TODO'"; + + @Builder.Default private final String downstreamSystem = null; + @Builder.Default private final URI instance = null; + @Builder.Default private final Integer downstreamStatus = null; + @Builder.Default private final String downstreamMessageId = null; + + @JsonIgnore @Builder.Default + private final transient List<ConstraintViolationApiDto> violations = null; +} diff --git a/lib/src/main/java/org/onap/portal/bff/mappers/ActionsMapper.java b/lib/src/main/java/org/onap/portal/bff/mappers/ActionsMapper.java new file mode 100644 index 0000000..588deba --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/mappers/ActionsMapper.java @@ -0,0 +1,37 @@ +/* + * + * 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.portal.bff.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.onap.portal.bff.config.MapperSpringConfig; +import org.onap.portal.bff.openapi.client_portal_history.model.ActionsListResponsePortalHistoryDto; +import org.onap.portal.bff.openapi.server.model.ActionsListResponseApiDto; +import org.springframework.core.convert.converter.Converter; + +@Mapper(config = MapperSpringConfig.class) +public interface ActionsMapper + extends Converter<ActionsListResponsePortalHistoryDto, ActionsListResponseApiDto> { + + @Mapping(source = "actionsList", target = "items") + ActionsListResponseApiDto convert(ActionsListResponsePortalHistoryDto source); +} diff --git a/lib/src/main/java/org/onap/portal/bff/mappers/CredentialMapper.java b/lib/src/main/java/org/onap/portal/bff/mappers/CredentialMapper.java new file mode 100644 index 0000000..e1db8de --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/mappers/CredentialMapper.java @@ -0,0 +1,33 @@ +/* + * + * 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.portal.bff.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.onap.portal.bff.config.MapperSpringConfig; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.CredentialKeycloakDto; +import org.onap.portal.bff.openapi.server.model.UpdateUserPasswordRequestApiDto; + +@Mapper(config = MapperSpringConfig.class, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface CredentialMapper { + CredentialKeycloakDto convert(UpdateUserPasswordRequestApiDto source); +} diff --git a/lib/src/main/java/org/onap/portal/bff/mappers/PreferencesMapper.java b/lib/src/main/java/org/onap/portal/bff/mappers/PreferencesMapper.java new file mode 100644 index 0000000..8a554fe --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/mappers/PreferencesMapper.java @@ -0,0 +1,37 @@ +/* + * + * 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.portal.bff.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.onap.portal.bff.config.MapperSpringConfig; +import org.onap.portal.bff.openapi.client_portal_prefs.model.PreferencesPortalPrefsDto; +import org.onap.portal.bff.openapi.server.model.PreferencesResponseApiDto; +import org.springframework.core.convert.converter.Converter; + +@Mapper(config = MapperSpringConfig.class) +public interface PreferencesMapper + extends Converter<PreferencesPortalPrefsDto, PreferencesResponseApiDto> { + + @Mapping(source = "properties", target = "properties") + PreferencesResponseApiDto convert(PreferencesPortalPrefsDto source); +} diff --git a/lib/src/main/java/org/onap/portal/bff/mappers/RolesMapper.java b/lib/src/main/java/org/onap/portal/bff/mappers/RolesMapper.java new file mode 100644 index 0000000..68d00d8 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/mappers/RolesMapper.java @@ -0,0 +1,36 @@ +/* + * + * 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.portal.bff.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.onap.portal.bff.config.MapperSpringConfig; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.RoleKeycloakDto; +import org.onap.portal.bff.openapi.server.model.RoleApiDto; +import org.springframework.core.convert.converter.Converter; + +@Mapper(config = MapperSpringConfig.class, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface RolesMapper extends Converter<RoleKeycloakDto, RoleApiDto> { + RoleApiDto convert(RoleKeycloakDto source); + + RoleKeycloakDto convert(RoleApiDto source); +} diff --git a/lib/src/main/java/org/onap/portal/bff/mappers/UsersMapper.java b/lib/src/main/java/org/onap/portal/bff/mappers/UsersMapper.java new file mode 100644 index 0000000..19cb4c7 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/mappers/UsersMapper.java @@ -0,0 +1,48 @@ +/* + * + * 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.portal.bff.mappers; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.onap.portal.bff.config.MapperSpringConfig; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.RequiredActionsKeycloakDto; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.UserKeycloakDto; +import org.onap.portal.bff.openapi.server.model.CreateUserRequestApiDto; +import org.onap.portal.bff.openapi.server.model.UpdateUserRequestApiDto; +import org.onap.portal.bff.openapi.server.model.UserResponseApiDto; +import org.springframework.core.convert.converter.Converter; + +@Mapper(config = MapperSpringConfig.class, unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface UsersMapper extends Converter<UserKeycloakDto, UserResponseApiDto> { + + UserResponseApiDto convert(UserKeycloakDto source); + + @Mapping(source = "roles", target = "realmRoles") + UserResponseApiDto convert(UserKeycloakDto source, List<String> roles); + + @Mapping(source = "actions", target = "requiredActions") + UserKeycloakDto convert(CreateUserRequestApiDto source, List<RequiredActionsKeycloakDto> actions); + + UserKeycloakDto convert(UpdateUserRequestApiDto source); +} diff --git a/lib/src/main/java/org/onap/portal/bff/services/ActionService.java b/lib/src/main/java/org/onap/portal/bff/services/ActionService.java new file mode 100644 index 0000000..0358d29 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/services/ActionService.java @@ -0,0 +1,125 @@ +/* + * + * 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.portal.bff.services; + +import lombok.RequiredArgsConstructor; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.client_portal_history.api.ActionsApi; +import org.onap.portal.bff.openapi.client_portal_history.model.CreateActionRequestPortalHistoryDto; +import org.onap.portal.bff.openapi.server.model.ActionsListResponseApiDto; +import org.onap.portal.bff.openapi.server.model.ActionsResponseApiDto; +import org.onap.portal.bff.openapi.server.model.CreateActionRequestApiDto; +import org.onap.portal.bff.openapi.server.model.ProblemApiDto; +import org.onap.portal.bff.utils.Logger; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Service +public class ActionService { + private final ActionsApi actionsApi; + private final ConfigurableConversionService conversionService; + + public Mono<ActionsResponseApiDto> createAction( + String userId, String xRequestId, CreateActionRequestApiDto createActionRequestApiDto) { + // First map from server API model to client API model + CreateActionRequestPortalHistoryDto createActionRequestPortalHistoryDto = + new CreateActionRequestPortalHistoryDto(); + createActionRequestPortalHistoryDto.setUserId(createActionRequestApiDto.getUserId()); + createActionRequestPortalHistoryDto.setAction(createActionRequestApiDto.getAction()); + createActionRequestPortalHistoryDto.setActionCreatedAt( + createActionRequestApiDto.getActionCreatedAt()); + + return actionsApi + .createAction(userId, xRequestId, createActionRequestPortalHistoryDto) + .map( + action -> + new ActionsResponseApiDto() + .action(action.getAction()) + .actionCreatedAt(action.getActionCreatedAt())) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Create actions failed for userId", + userId, + ProblemApiDto.DownstreamSystemEnum.PORTAL_HISTORY.toString()); + return Mono.error(ex); + }); + } + + public Mono<ActionsListResponseApiDto> getActions( + String userId, String xRequestId, Integer page, Integer pageSize, Integer showLastHours) { + + return actionsApi + .getActions(userId, xRequestId, page, pageSize, showLastHours) + .map(actions -> conversionService.convert(actions, ActionsListResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Get actions failed for userId", + userId, + ProblemApiDto.DownstreamSystemEnum.PORTAL_HISTORY.toString()); + return Mono.error(ex); + }); + } + + public Mono<ActionsListResponseApiDto> listActions( + String xRequestId, Integer page, Integer pageSize, Integer showLast) { + return actionsApi + .listActions(xRequestId, page, pageSize, showLast) + .map( + responseEntity -> + conversionService.convert(responseEntity, ActionsListResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "List actions failed", + null, + ProblemApiDto.DownstreamSystemEnum.PORTAL_HISTORY.toString()); + return Mono.error(ex); + }); + } + + public Mono<Object> deleteActions(String userId, String xRequestId, Integer deleteAfterHours) { + return actionsApi + .deleteActions(userId, xRequestId, deleteAfterHours) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Get actions failed for userId because actions cannot be deleted after " + + deleteAfterHours + + " hours", + userId, + ProblemApiDto.DownstreamSystemEnum.PORTAL_HISTORY.toString()); + return Mono.error(ex); + }); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/services/KeycloakService.java b/lib/src/main/java/org/onap/portal/bff/services/KeycloakService.java new file mode 100644 index 0000000..ff96b63 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/services/KeycloakService.java @@ -0,0 +1,389 @@ +/* + * + * 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.portal.bff.services; + +import io.vavr.API; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.List; +import io.vavr.control.Option; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.mappers.CredentialMapper; +import org.onap.portal.bff.mappers.RolesMapper; +import org.onap.portal.bff.mappers.UsersMapper; +import org.onap.portal.bff.openapi.client_portal_keycloak.api.KeycloakApi; +import org.onap.portal.bff.openapi.client_portal_keycloak.model.RequiredActionsKeycloakDto; +import org.onap.portal.bff.openapi.server.model.*; +import org.onap.portal.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; + +@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 = + Option.of(request.getRoles()).fold(List::empty, List::ofAll); + return listRoles(xRequestId) + .collectList() + .flatMap( + realmRoles -> { + final List<RoleApiDto> absentRoles = + rolesToBeAssigned.filter(role -> !realmRoles.contains(role)); + if (!absentRoles.isEmpty()) { + return Mono.error( + DownstreamApiProblemException.builder() + .status(Status.NOT_FOUND) + .detail( + String.format( + "Roles not found in the realm: %s", + absentRoles.map(RoleApiDto::getName).asJava())) + .downstreamSystem(ProblemApiDto.DownstreamSystemEnum.KEYCLOAK.toString()) + .title(HttpStatus.NOT_FOUND.toString()) + .build()); + } + return Mono.just(rolesToBeAssigned); + }) + .flatMap(roles -> createUserWithRoles(request, xRequestId, List.ofAll(roles))); + } + + private Mono<UserResponseApiDto> createUserWithRoles( + CreateUserRequestApiDto request, String xRequestId, List<RoleApiDto> roles) { + return keycloakApi + .createUserWithHttpInfo( + usersMapper.convert( + request, List.of(RequiredActionsKeycloakDto.UPDATE_PASSWORD).asJava())) + .flatMap( + responseEntity -> + Option.of(responseEntity.getHeaders().getLocation()) + .map(URI::toString) + .map(location -> location.substring(location.lastIndexOf("/") + 1)) + .fold( + () -> Mono.error(DownstreamApiProblemException.builder().build()), + Mono::just)) + .flatMap( + userId -> { + if (!roles.isEmpty()) { + return assignRoles(userId, roles); + } + return Mono.just(userId); + }) + .flatMap( + userId -> + sendActionEmail( + userId, API.List(RequiredActionsKeycloakDto.UPDATE_PASSWORD).toJavaList())) + .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; + + 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(), + listRoles(xRequestId) + .flatMap( + role -> + listUsersByRole(role.getName(), xRequestId) + .map(user -> Tuple.of(user.getId(), role.getName()))) + .collectList() + .map(List::ofAll) + .map(list -> list.groupBy(t -> t._1).map((k, v) -> Tuple.of(k, v.map(Tuple2::_2))))) + .map( + tuple -> { + final UserListResponseApiDto result = new UserListResponseApiDto(); + result.setTotalCount(tuple.getT1()); + result.setItems( + List.ofAll(tuple.getT2()) + .map( + user -> + usersMapper.convert( + user, + tuple.getT3().getOrElse(user.getId(), API.List()).toJavaList())) + .toJavaList()); + + 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.map(RoleApiDto::getId).mkString(", ")); + + return keycloakApi + .addRealmRoleMappingsToUser(userId, roles.map(rolesMapper::convert).toJavaList()) + .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.map(RoleApiDto::getId).mkString(", ")); + + return getAssignedRoles(userId, xRequestId) + .map(response -> List.ofAll(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.map(RoleApiDto::getId).mkString(", ")); + + return keycloakApi.deleteRealmRoleMappingsByUserId( + userId, roles.map(rolesMapper::convert).toJavaList()); + } + + 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) + .log() + .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); + }); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/services/PreferencesService.java b/lib/src/main/java/org/onap/portal/bff/services/PreferencesService.java new file mode 100644 index 0000000..ee0a5df --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/services/PreferencesService.java @@ -0,0 +1,91 @@ +/* + * + * 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.portal.bff.services; + +import lombok.RequiredArgsConstructor; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.client_portal_prefs.api.PreferencesApi; +import org.onap.portal.bff.openapi.client_portal_prefs.model.PreferencesPortalPrefsDto; +import org.onap.portal.bff.openapi.server.model.CreatePreferencesRequestApiDto; +import org.onap.portal.bff.openapi.server.model.PreferencesResponseApiDto; +import org.onap.portal.bff.utils.Logger; +import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@Service +public class PreferencesService { + + private static final String PREFERENCES_APPLICATION_NAME = "PORTAL_PREFS"; + + private final PreferencesApi preferencesApi; + private final ConfigurableConversionService conversionService; + + public Mono<PreferencesResponseApiDto> createPreferences( + String xRequestId, CreatePreferencesRequestApiDto request) { + PreferencesPortalPrefsDto preferencesPortalPrefsDto = new PreferencesPortalPrefsDto(); + preferencesPortalPrefsDto.setProperties(request.getProperties()); + return preferencesApi + .savePreferences(xRequestId, preferencesPortalPrefsDto) + .map(resp -> conversionService.convert(resp, PreferencesResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, "Preference raise error", xRequestId, PREFERENCES_APPLICATION_NAME); + return Mono.error(ex); + }); + } + + public Mono<PreferencesResponseApiDto> updatePreferences( + String xRequestId, CreatePreferencesRequestApiDto request) { + PreferencesPortalPrefsDto preferencesPortalPrefsDto = new PreferencesPortalPrefsDto(); + preferencesPortalPrefsDto.setProperties(request.getProperties()); + return preferencesApi + .updatePreferences(xRequestId, preferencesPortalPrefsDto) + .map(resp -> conversionService.convert(resp, PreferencesResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, "Preference raise error", xRequestId, PREFERENCES_APPLICATION_NAME); + return Mono.error(ex); + }); + } + + public Mono<PreferencesResponseApiDto> getPreferences(String xRequestId) { + return preferencesApi + .getPreferences(xRequestId) + .map(preferences -> conversionService.convert(preferences, PreferencesResponseApiDto.class)) + .onErrorResume( + DownstreamApiProblemException.class, + ex -> { + Logger.errorLog( + xRequestId, + "Get preferences failed for ID", + xRequestId, + PREFERENCES_APPLICATION_NAME); + return Mono.error(ex); + }); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/utils/ErrorHandler.java b/lib/src/main/java/org/onap/portal/bff/utils/ErrorHandler.java new file mode 100644 index 0000000..8bec189 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/utils/ErrorHandler.java @@ -0,0 +1,65 @@ +/* + * + * 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.portal.bff.utils; + +import java.util.List; +import java.util.Objects; +import org.onap.portal.bff.exceptions.DownstreamApiProblemException; +import org.onap.portal.bff.openapi.server.model.ProblemApiDto; +import org.springframework.http.HttpStatus; + +public class ErrorHandler { + /** + * Not meant to be instantiated. To prevent Java from adding an implicit public constructor to + * every class which does not define at least one explicitly. + */ + private ErrorHandler() {} + + public static String mapVariablesToDetails(List<String> variables, String details) { + int i = 0; + for (String variable : variables) { + i++; + details = details.replace("%" + i, variable); + } + return details; + } + + public static DownstreamApiProblemException getDownstreamApiProblemException( + HttpStatus httpStatus, + List<String> variables, + String text, + String messageId, + ProblemApiDto.DownstreamSystemEnum downStreamSystem) { + String errorDetail = + variables != null && text != null + ? ErrorHandler.mapVariablesToDetails(variables, text) + : null; + + return DownstreamApiProblemException.builder() + .title(httpStatus.toString()) + .detail(errorDetail) + .downstreamMessageId(Objects.requireNonNullElse(messageId, "not set by downstream system")) + .downstreamSystem(downStreamSystem.toString()) + .downstreamStatus(httpStatus.value()) + .build(); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/utils/Logger.java b/lib/src/main/java/org/onap/portal/bff/utils/Logger.java new file mode 100644 index 0000000..b985ad5 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/utils/Logger.java @@ -0,0 +1,61 @@ +/* + * + * 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.portal.bff.utils; + +import java.net.URI; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; + +@Slf4j +public class Logger { + + /** + * Not meant to be instantiated. To prevent Java from adding an implicit public constructor to + * every class which does not define at least one explicitly. + */ + private Logger() {} + + public static void requestLog(String xRequestId, HttpMethod methode, URI path) { + log.info("Portal-bff - request - X-Request-Id {} {} {}", xRequestId, methode, path); + } + + public static void responseLog(String xRequestId, HttpStatus code) { + log.info("Portal-bff - response - X-Request-Id {} {}", xRequestId, code); + } + + public static void errorLog(String xRequestId, String msg, String id, String app) { + log.info( + "Portal-bff - error - X-Request-Id {} {} {} not found in {}", xRequestId, msg, id, app); + } + + public static void errorLog( + String xRequestId, String msg, String id, String app, String errorDetails) { + log.info( + "Portal-bff - error - X-Request-Id {} {} {} not found in {} error message: {}", + xRequestId, + msg, + id, + app, + errorDetails); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/utils/SortingChainResolver.java b/lib/src/main/java/org/onap/portal/bff/utils/SortingChainResolver.java new file mode 100644 index 0000000..d162637 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/utils/SortingChainResolver.java @@ -0,0 +1,55 @@ +/* + * + * 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.portal.bff.utils; + +import io.vavr.collection.Map; +import io.vavr.collection.Seq; +import io.vavr.control.Option; +import java.util.Comparator; + +public class SortingChainResolver<T> { + final Map<String, Comparator<T>> comparators; + + public SortingChainResolver(Map<String, Comparator<T>> comparators) { + this.comparators = comparators; + } + + public Option<Comparator<T>> resolve(Seq<SortingParser.SortingParam> sortingParams) { + final Seq<Comparator<T>> resolvedComparators = + sortingParams.flatMap( + sortingParam -> + comparators + .get(sortingParam.getName()) + .map( + comparator -> { + if (sortingParam.isDescending()) { + return comparator.reversed(); + } + return comparator; + })); + + if (resolvedComparators.isEmpty()) { + return Option.none(); + } + return Option.some(resolvedComparators.reduceLeft(Comparator::thenComparing)); + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/utils/SortingParser.java b/lib/src/main/java/org/onap/portal/bff/utils/SortingParser.java new file mode 100644 index 0000000..d08f775 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/utils/SortingParser.java @@ -0,0 +1,57 @@ +/* + * + * 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.portal.bff.utils; + +import io.vavr.collection.List; +import io.vavr.collection.Seq; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +public class SortingParser { + private static final String DESC_PREFIX = "-"; + private static final String SEPARATOR = ","; + + private SortingParser() {} + + public static Seq<SortingParam> parse(String sort) { + return List.of(sort.split(SEPARATOR)) + .filter(name -> !name.isEmpty() && !name.equals(DESC_PREFIX)) + .map( + name -> { + if (name.startsWith(DESC_PREFIX)) { + return SortingParam.builder() + .name(name.substring(DESC_PREFIX.length())) + .isDescending(true) + .build(); + } + return SortingParam.builder().name(name).isDescending(false).build(); + }); + } + + @Builder + @Value + public static class SortingParam { + @NonNull String name; + boolean isDescending; + } +} diff --git a/lib/src/main/java/org/onap/portal/bff/utils/VersionComparator.java b/lib/src/main/java/org/onap/portal/bff/utils/VersionComparator.java new file mode 100644 index 0000000..cb8ecf1 --- /dev/null +++ b/lib/src/main/java/org/onap/portal/bff/utils/VersionComparator.java @@ -0,0 +1,48 @@ +/* + * + * 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.portal.bff.utils; + +import java.util.Comparator; +import java.util.regex.Pattern; + +public class VersionComparator implements Comparator<String> { + private static final Pattern SEPARATOR_PATTERN = Pattern.compile("\\."); + + @Override + public int compare(String version1, String version2) { + final String[] parsedVersion1 = SEPARATOR_PATTERN.split(version1); + final String[] parsedVersion2 = SEPARATOR_PATTERN.split(version2); + final int maxLength = Math.max(parsedVersion1.length, parsedVersion2.length); + + for (int i = 0; i < maxLength; i++) { + final Integer v1 = i < parsedVersion1.length ? Integer.parseInt(parsedVersion1[i]) : 0; + final Integer v2 = i < parsedVersion2.length ? Integer.parseInt(parsedVersion2[i]) : 0; + final int compare = v1.compareTo(v2); + + if (compare != 0) { + return compare; + } + } + + return 0; + } +} |