diff options
author | Fiete Ostkamp <Fiete.Ostkamp@telekom.de> | 2023-04-14 11:44:19 +0000 |
---|---|---|
committer | Fiete Ostkamp <Fiete.Ostkamp@telekom.de> | 2023-04-14 11:44:19 +0000 |
commit | cdc670c5a1c25b0b0ab460b1711a0a42f270b1f3 (patch) | |
tree | 41ac6c0e7a52505fd1d0de057df6d5328a853cd0 /lib/src/main/java | |
parent | 1a9b563662e9a9dd1f89e04ce0026e2cc5c4771d (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')
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; + } +} |