aboutsummaryrefslogtreecommitdiffstats
path: root/lib/src/main/java
diff options
context:
space:
mode:
authorFiete Ostkamp <Fiete.Ostkamp@telekom.de>2023-04-14 11:44:19 +0000
committerFiete Ostkamp <Fiete.Ostkamp@telekom.de>2023-04-14 11:44:19 +0000
commitcdc670c5a1c25b0b0ab460b1711a0a42f270b1f3 (patch)
tree41ac6c0e7a52505fd1d0de057df6d5328a853cd0 /lib/src/main/java
parent1a9b563662e9a9dd1f89e04ce0026e2cc5c4771d (diff)
Upload bff
Issue-ID: PORTAL-1083 Signed-off-by: Fiete Ostkamp <Fiete.Ostkamp@telekom.de> Change-Id: I50f0a2db2dab28354c32c1ebf5a5e22afb0faade
Diffstat (limited to 'lib/src/main/java')
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/BeansConfig.java191
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/ConversionServiceConfig.java59
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/IdTokenExchangeFilterFunction.java125
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/LoggerInterceptor.java52
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/MapperSpringConfig.java28
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/PortalBffConfig.java63
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/SecurityConfig.java77
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/clients/AbstractClientConfig.java87
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/clients/KeycloakConfig.java104
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/clients/PortalHistoryConfig.java97
-rw-r--r--lib/src/main/java/org/onap/portal/bff/config/clients/PortalPrefsConfig.java96
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/AbstractBffController.java46
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/ActionsController.java84
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/BffControllerAdvice.java28
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/PreferencesController.java77
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/RolesController.java56
-rw-r--r--lib/src/main/java/org/onap/portal/bff/controller/UsersController.java145
-rw-r--r--lib/src/main/java/org/onap/portal/bff/exceptions/DownstreamApiProblemException.java65
-rw-r--r--lib/src/main/java/org/onap/portal/bff/mappers/ActionsMapper.java37
-rw-r--r--lib/src/main/java/org/onap/portal/bff/mappers/CredentialMapper.java33
-rw-r--r--lib/src/main/java/org/onap/portal/bff/mappers/PreferencesMapper.java37
-rw-r--r--lib/src/main/java/org/onap/portal/bff/mappers/RolesMapper.java36
-rw-r--r--lib/src/main/java/org/onap/portal/bff/mappers/UsersMapper.java48
-rw-r--r--lib/src/main/java/org/onap/portal/bff/services/ActionService.java125
-rw-r--r--lib/src/main/java/org/onap/portal/bff/services/KeycloakService.java389
-rw-r--r--lib/src/main/java/org/onap/portal/bff/services/PreferencesService.java91
-rw-r--r--lib/src/main/java/org/onap/portal/bff/utils/ErrorHandler.java65
-rw-r--r--lib/src/main/java/org/onap/portal/bff/utils/Logger.java61
-rw-r--r--lib/src/main/java/org/onap/portal/bff/utils/SortingChainResolver.java55
-rw-r--r--lib/src/main/java/org/onap/portal/bff/utils/SortingParser.java57
-rw-r--r--lib/src/main/java/org/onap/portal/bff/utils/VersionComparator.java48
31 files changed, 2562 insertions, 0 deletions
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;
+ }
+}