From b146259516cc90cc9084bdf2f69c358b896cfdf7 Mon Sep 17 00:00:00 2001 From: Fiete Ostkamp Date: Tue, 11 Jul 2023 08:44:51 +0000 Subject: history repo code is missing Issue-ID: PORTALNG-8 Signed-off-by: Fiete Ostkamp Change-Id: I01f1789eb840661115bfd806a0622d02666100c0 --- .../portal/history/PortalHistoryApplication.java | 39 ++++ .../portal/history/configuration/BeansConfig.java | 50 +++++ .../portal/history/configuration/Errorhandler.java | 94 +++++++++ .../history/configuration/LogInterceptor.java | 62 ++++++ .../history/configuration/PortalHistoryConfig.java | 37 ++++ .../history/configuration/SchedulerConfig.java | 55 ++++++ .../history/configuration/SecurityConfig.java | 53 +++++ .../portal/history/configuration/package-info.java | 25 +++ .../history/controller/ActionsController.java | 88 +++++++++ .../onap/portal/history/entities/ActionsDao.java | 44 +++++ .../portal/history/exception/ProblemException.java | 55 ++++++ .../history/repository/ActionsRepository.java | 42 ++++ .../portal/history/services/ActionsService.java | 218 +++++++++++++++++++++ .../onap/portal/history/util/IdTokenExchange.java | 126 ++++++++++++ .../java/org/onap/portal/history/util/Logger.java | 64 ++++++ app/src/main/resources/application-local.yml | 42 ++++ app/src/main/resources/application.yml | 40 ++++ app/src/main/resources/logback-spring.xml | 15 ++ 18 files changed, 1149 insertions(+) create mode 100644 app/src/main/java/org/onap/portal/history/PortalHistoryApplication.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/BeansConfig.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/Errorhandler.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/LogInterceptor.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/PortalHistoryConfig.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/SchedulerConfig.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/SecurityConfig.java create mode 100644 app/src/main/java/org/onap/portal/history/configuration/package-info.java create mode 100644 app/src/main/java/org/onap/portal/history/controller/ActionsController.java create mode 100644 app/src/main/java/org/onap/portal/history/entities/ActionsDao.java create mode 100644 app/src/main/java/org/onap/portal/history/exception/ProblemException.java create mode 100644 app/src/main/java/org/onap/portal/history/repository/ActionsRepository.java create mode 100644 app/src/main/java/org/onap/portal/history/services/ActionsService.java create mode 100644 app/src/main/java/org/onap/portal/history/util/IdTokenExchange.java create mode 100644 app/src/main/java/org/onap/portal/history/util/Logger.java create mode 100644 app/src/main/resources/application-local.yml create mode 100644 app/src/main/resources/application.yml create mode 100644 app/src/main/resources/logback-spring.xml (limited to 'app/src/main') diff --git a/app/src/main/java/org/onap/portal/history/PortalHistoryApplication.java b/app/src/main/java/org/onap/portal/history/PortalHistoryApplication.java new file mode 100644 index 0000000..0e712f2 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/PortalHistoryApplication.java @@ -0,0 +1,39 @@ +/* + * + * 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 + * + * + */ + +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package org.onap.portal.history; + + +import org.onap.portal.history.configuration.PortalHistoryConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +@EnableConfigurationProperties(PortalHistoryConfig.class) +@SpringBootApplication +public class PortalHistoryApplication { + public static void main(String[] args) { + SpringApplication.run(PortalHistoryApplication.class, args); + } +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/BeansConfig.java b/app/src/main/java/org/onap/portal/history/configuration/BeansConfig.java new file mode 100644 index 0000000..9a60681 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/BeansConfig.java @@ -0,0 +1,50 @@ +/* + * + * 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.history.configuration; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.zalando.problem.jackson.ProblemModule; + +import java.time.Clock; + +@Configuration +public class BeansConfig { + @Bean + Clock clock() { + return Clock.systemUTC(); + } + + @Bean + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + return builder + .modules(new ProblemModule(), new JavaTimeModule()) + .build() + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/Errorhandler.java b/app/src/main/java/org/onap/portal/history/configuration/Errorhandler.java new file mode 100644 index 0000000..583420b --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/Errorhandler.java @@ -0,0 +1,94 @@ +/* + * + * 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.history.configuration; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.onap.portal.history.exception.ProblemException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import reactor.core.publisher.Mono; + +@Component +public class Errorhandler implements ErrorWebExceptionHandler { + + @Autowired + ObjectMapper objectMapper; + + /** + * Override the handle methode to implement custom error handling + * Set response status code to BAD REQUEST, set header content-type and fill the body with the Problem object along the API model + */ + @Override + public Mono handle(ServerWebExchange exchange, Throwable ex) { + ServerHttpResponse httpResponse = exchange.getResponse(); + setResponseStatus(httpResponse, ex); + httpResponse.getHeaders().add("Content-Type", "application/problem+json"); + return httpResponse.writeWith(Mono.fromSupplier(() -> { + DataBufferFactory bufferFactory = httpResponse.bufferFactory(); + try { + return + (httpResponse.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) + ? httpResponse.bufferFactory().wrap(objectMapper.writeValueAsBytes(setProblemException(httpResponse, ex.getMessage()))) + : httpResponse.bufferFactory().wrap(objectMapper.writeValueAsBytes(ex)); + } catch (JsonProcessingException e) { + return bufferFactory.wrap(new byte[0]); + } + })); + } + + /** + * Set the response status + * @param httpResponse response which status code should be set + * @param ex throwable exception to identify the Problem class + */ + private void setResponseStatus(ServerHttpResponse httpResponse, Throwable ex) { + if (ex instanceof Problem) { + httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); + } else { + httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + /** + * Build a problem exception and set the response status code to BAD REQUEST for every response + * @param httpResponse response which status code should be set + * @param message for the detail of the problem exception + * @return problem exception instance + */ + private ProblemException setProblemException(ServerHttpResponse httpResponse, String message){ + httpResponse.setStatusCode(HttpStatus.BAD_REQUEST); + return ProblemException.builder() + .status(Status.INTERNAL_SERVER_ERROR) + .title(Status.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .detail(message) + .build(); + + } +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/LogInterceptor.java b/app/src/main/java/org/onap/portal/history/configuration/LogInterceptor.java new file mode 100644 index 0000000..113aad8 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/LogInterceptor.java @@ -0,0 +1,62 @@ +/* + * + * 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.history.configuration; + +import org.onap.portal.history.util.Logger; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class LogInterceptor implements WebFilter { + public static final String EXCHANGE_CONTEXT_ATTRIBUTE = + ServerWebExchangeContextFilter.class.getName() + ".EXCHANGE_CONTEXT"; + + public static final String X_REQUEST_ID = "X-Request-Id"; + + /** + * Override a web filter to write log entries for every request and response and add header in response with X_REQUEST_ID + */ + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + List 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); + exchange.getResponse().beforeCommit(() -> { + Logger.responseLog(xRequestId,exchange.getResponse().getStatusCode()); + return Mono.empty(); + }); + } + + return chain + .filter(exchange) + .contextWrite(cxt -> cxt.put(EXCHANGE_CONTEXT_ATTRIBUTE, exchange)); + } +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/PortalHistoryConfig.java b/app/src/main/java/org/onap/portal/history/configuration/PortalHistoryConfig.java new file mode 100644 index 0000000..85304b9 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/PortalHistoryConfig.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.history.configuration; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +import javax.validation.constraints.NotBlank; + +@Data +@ConstructorBinding +@ConfigurationProperties("portal-history") +public class PortalHistoryConfig { + + @NotBlank + private final Integer saveInterval; +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/SchedulerConfig.java b/app/src/main/java/org/onap/portal/history/configuration/SchedulerConfig.java new file mode 100644 index 0000000..529cbc3 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/SchedulerConfig.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.history.configuration; + +import org.onap.portal.history.services.ActionsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@EnableScheduling +public class SchedulerConfig { + + private final ActionsService actionsService; + private final PortalHistoryConfig portalHistoryConfig; + + @Autowired + public SchedulerConfig(ActionsService actionsService, PortalHistoryConfig portalHistoryConfig){ + this.actionsService = actionsService; + this.portalHistoryConfig = portalHistoryConfig; + } + + /** + * This method will be trigger by Spring Boot scheduler. + * The cron execution time is configured in the application properties as well as the save interval. + */ + @Scheduled(cron="${portal-history.delete-interval}") + public void runDeleteActions(){ + actionsService.deleteActions(portalHistoryConfig.getSaveInterval()); + log.info("Delete actions in scheduled job"); + } +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/SecurityConfig.java b/app/src/main/java/org/onap/portal/history/configuration/SecurityConfig.java new file mode 100644 index 0000000..e825295 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/SecurityConfig.java @@ -0,0 +1,53 @@ +/* + * + * 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.history.configuration; + +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.web.server.SecurityWebFilterChain; + +/** + * Configures the access control of the API endpoints. + */ +// https://hantsy.github.io/spring-reactive-sample/security/config.html +@EnableWebFluxSecurity +@Configuration +public class SecurityConfig { + + @Bean + public SecurityWebFilterChain springSecurityWebFilterChain(ServerHttpSecurity http) { + return http.httpBasic().disable() + .formLogin().disable() + .csrf().disable() + .cors() + .and() + .authorizeExchange() + .pathMatchers(HttpMethod.GET, "/actuator/**").permitAll() + .anyExchange().authenticated() + .and() + .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::jwt) + .build(); + } +} diff --git a/app/src/main/java/org/onap/portal/history/configuration/package-info.java b/app/src/main/java/org/onap/portal/history/configuration/package-info.java new file mode 100644 index 0000000..ccaa303 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/configuration/package-info.java @@ -0,0 +1,25 @@ +/* + * + * 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 + * + * + */ + +@ParametersAreNonnullByDefault +package org.onap.portal.history.configuration; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/app/src/main/java/org/onap/portal/history/controller/ActionsController.java b/app/src/main/java/org/onap/portal/history/controller/ActionsController.java new file mode 100644 index 0000000..9fd9f79 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/controller/ActionsController.java @@ -0,0 +1,88 @@ +/* + * + * 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.history.controller; + +import java.util.Optional; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +import org.onap.portal.history.configuration.PortalHistoryConfig; +import org.onap.portal.history.openapi.api.ActionsApi; +import org.onap.portal.history.openapi.model.ActionResponse; +import org.onap.portal.history.openapi.model.ActionsListResponse; +import org.onap.portal.history.openapi.model.CreateActionRequest; +import org.onap.portal.history.services.ActionsService; +import org.onap.portal.history.util.IdTokenExchange; +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 implements ActionsApi { + + private final ActionsService actionsService; + private final PortalHistoryConfig portalHistoryConfig; + + public ActionsController(ActionsService actionsService, PortalHistoryConfig portalHistoryConfig){ + this.actionsService = actionsService; + this.portalHistoryConfig = portalHistoryConfig; + } + + @Override + public Mono> createAction(String userId, String xRequestId, Mono createActionRequest, ServerWebExchange exchange) { + + return IdTokenExchange + .validateUserId(userId, exchange, xRequestId) + .then(createActionRequest.flatMap(action -> actionsService.createActions(userId, action, portalHistoryConfig.getSaveInterval(), xRequestId))) + .map(ResponseEntity::ok); + } + + @Override + public Mono> deleteActions(String userId, String xRequestId, Integer deleteAfterHours, ServerWebExchange exchange) { + + return IdTokenExchange + .validateUserId(userId, exchange, xRequestId) + .then(actionsService.deleteUserActions(userId, deleteAfterHours, xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono> getActions(String userId, String xRequestId, Optional page, Optional pageSize, Optional showLastHours, ServerWebExchange exchange) { + + return IdTokenExchange + .validateUserId(userId, exchange, xRequestId) + .then(actionsService.getActions(userId, page.orElse(1), pageSize.orElse(10), showLastHours.orElse(portalHistoryConfig.getSaveInterval()), portalHistoryConfig.getSaveInterval(), xRequestId)) + .map(ResponseEntity::ok); + } + + @Override + public Mono> listActions(String xRequestId, @Valid Optional<@Min(1) Integer> page, @Valid Optional<@Min(1) @Max(5000) Integer> pageSize, @Valid Optional showLastHours, ServerWebExchange exchange) { + + return actionsService + .listActions(page.orElse(1), pageSize.orElse(10), showLastHours.orElse(portalHistoryConfig.getSaveInterval()), portalHistoryConfig.getSaveInterval(), xRequestId) + .map(ResponseEntity::ok); + } +} diff --git a/app/src/main/java/org/onap/portal/history/entities/ActionsDao.java b/app/src/main/java/org/onap/portal/history/entities/ActionsDao.java new file mode 100644 index 0000000..5457e87 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/entities/ActionsDao.java @@ -0,0 +1,44 @@ +/* + * + * 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.history.entities; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.mongodb.core.mapping.Document; +import java.util.Date; + +/** + * Data access object for the actions in the MongoDB repository. + * No database id is set in this class because MongoDB use internal _id as primary key / uniq object identifier + */ +@Document(collection = "actions") +@Getter +@Setter +public class ActionsDao { + + private String userId; + + private Date actionCreatedAt; + + private Object action; + +} diff --git a/app/src/main/java/org/onap/portal/history/exception/ProblemException.java b/app/src/main/java/org/onap/portal/history/exception/ProblemException.java new file mode 100644 index 0000000..f51d246 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/exception/ProblemException.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.history.exception; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.zalando.problem.AbstractThrowableProblem; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import org.zalando.problem.StatusType; + +import java.net.URI; + +/** + * Default problem exception. This class has the same structure as the problem response model from the api. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class ProblemException extends AbstractThrowableProblem { + @Builder.Default private final URI type = Problem.DEFAULT_TYPE; + + @Builder.Default private final String title = "Bad history error"; + + @Builder.Default private final StatusType status = Status.BAD_REQUEST; + + @Builder.Default private final String detail = "Please add more details here"; + + @Builder.Default private final URI instance = null; + +} diff --git a/app/src/main/java/org/onap/portal/history/repository/ActionsRepository.java b/app/src/main/java/org/onap/portal/history/repository/ActionsRepository.java new file mode 100644 index 0000000..79fc378 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/repository/ActionsRepository.java @@ -0,0 +1,42 @@ +/* + * + * 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.history.repository; + +import java.util.Date; + +import org.onap.portal.history.entities.ActionsDao; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface ActionsRepository extends ReactiveMongoRepository { + + Flux findAllByActionCreatedAtAfter(Pageable pageable, Date actionCreatedAt); + + Flux findAllByUserIdAndActionCreatedAtAfter(Pageable pageable, String userId, Date actionCreatedAt); + + Mono deleteAllByUserIdAndActionCreatedAtIsBefore(String userId, Date actionCreatedAt); + + Mono deleteAllByActionCreatedAtIsBefore(Date actionCreatedAt); +} diff --git a/app/src/main/java/org/onap/portal/history/services/ActionsService.java b/app/src/main/java/org/onap/portal/history/services/ActionsService.java new file mode 100644 index 0000000..a14fef2 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/services/ActionsService.java @@ -0,0 +1,218 @@ +/* + * + * 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.history.services; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Date; + +import org.onap.portal.history.entities.ActionsDao; +import org.onap.portal.history.exception.ProblemException; +import org.onap.portal.history.openapi.model.ActionResponse; +import org.onap.portal.history.openapi.model.ActionsListResponse; +import org.onap.portal.history.openapi.model.CreateActionRequest; +import org.onap.portal.history.repository.ActionsRepository; +import org.onap.portal.history.util.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; + +import lombok.extern.slf4j.Slf4j; +import reactor.core.publisher.Mono; + + +@Slf4j +@Service +public class ActionsService { + + @Autowired + private ActionsRepository repository; + + /** + * Retrieve actions for a given userId from the database and provide a list with actions + * @param userId only actions for this userId should be retrieved + * @param page which page should be retrieved from the list of actions. From a user perspective the first page has the page number 1. + * In the response list the first page starts with 0. Therefore, a subtraction is needed. + * @param pageSize length of the response list + * @param showLastHours for which hours from the current time the actions should be retrieved. + * @param saveInterval value will be part of the response action object. This value is set in the application properties. + * In the future this value can be provided from the client. + * @param xRequestId from the request header. Will be used in an error log + * @return If successful object with an item list of action objects and an item with the list count, otherwise Mono error + */ + public Mono getActions(String userId, Integer page, Integer pageSize, Integer showLastHours, Integer saveInterval, String xRequestId){ + Pageable paging = PageRequest.of(page - 1 , pageSize, Sort.by(Sort.Direction.DESC, "actionCreatedAt")); + var dateAfter = Date.from(ZonedDateTime.now().minusHours(showLastHours).toInstant()); + return repository + .findAllByUserIdAndActionCreatedAtAfter(paging,userId, dateAfter) + .map(actionDao -> toActionResponse(actionDao, saveInterval)) + .collectList() + .map(this::toActionsListResponse) + .switchIfEmpty(Mono.just(new ActionsListResponse().totalCount(0))) + .onErrorResume(ex -> { + Logger.errorLog(xRequestId,"Get actions cannot be executed for user with id ", userId); + return getError("Get actions can not be executed for user with id " + userId); + }); + } + + /** + * Create an action data record in the database + * @param userId the id of the user for which the action should be stored + * @param createActionRequest the action object which should be stored + * @param saveInterval value will be part of the response action object. This value is set in the application properties. + * In the future this value can be provided from the client. + * @param xRequestId from the request header. Will be used in an error log + * @return If successful object with the stored action, otherwise Mono error + */ + public Mono createActions(String userId, CreateActionRequest createActionRequest, Integer saveInterval, String xRequestId) { + return repository + .save(toActionsDao(userId, createActionRequest)) + .map(action -> toActionResponse(action, saveInterval)) + .onErrorResume(ex -> { + Logger.errorLog(xRequestId,"Action for user can not be executed for user with id ", userId ); + return Mono.error(ProblemException.builder() + .type(Problem.DEFAULT_TYPE) + .status(Status.BAD_REQUEST) + .title(HttpStatus.BAD_REQUEST.toString()) + .detail("Action for user can not be executed for user with id " + userId) + .build()); + }); + } + + /** + * List all actions without a userId filter. + * @param page which page should be retrieved from the list of actions. From a user perspective the first page has the page number 1. + * In the response list the first page starts with 0. Therefore, a subtraction is needed. + * @param pageSize length of the response list + * @param showLastHours for which hours from the current time the actions should be retrieved. + * @param saveInterval value will be part of the response action object. This value is set in the application properties. + * * In the future this value can be provided from the client. + * @param xRequestId from the request header. Will be used in an error log + * @return If successful list with action response object, otherwise Mono error + */ + public Mono listActions(Integer page, Integer pageSize, Integer showLastHours, Integer saveInterval, String xRequestId){ + + var paging = PageRequest.of(page - 1 , pageSize, Sort.by(Sort.Direction.DESC, "actionCreatedAt")); + var dateAfter = Date.from(ZonedDateTime.now().minusHours(showLastHours).toInstant()); + + return repository + .findAllByActionCreatedAtAfter(paging,dateAfter) + .map(actionDto -> toActionResponse(actionDto, saveInterval)) + .collectList() + .map(this::toActionsListResponse) + .onErrorResume(ProblemException.class, + ex -> { + Logger.errorLog(xRequestId,"List actions cannot be created", null ); + return getError("List actions cannot be created"); + }); + } + + /** + * Delete actions for a given userId and action is create after hours + * @param userId the id of the user for which the action should be deleted + * @param deleteAfterHours hours after the actions should be deleted + * @param xRequestId from the request header. Will be used in an error log + * @return If successful empty Mono object, otherwise Mono error + */ + public Mono deleteUserActions(String userId, Integer deleteAfterHours, String xRequestId ){ + var dateAfter = Date.from(ZonedDateTime.now().minusHours(deleteAfterHours).toInstant()); + return repository + .deleteAllByUserIdAndActionCreatedAtIsBefore(userId, dateAfter) + .map(resp -> new Object()) + .onErrorResume(ProblemException.class,ex -> { + Logger.errorLog(xRequestId,"Deletion of actions cannot be executed for user", userId ); + return Mono.error(ex); + }); + } + + /** + * Delete actions after hours. This service will be used in the cron job. The job will be implemented with a separate user story. + * @param deleteAfterHours hours after the actions should be deleted + * @return If successful empty Mono object, otherwise Mono error + */ + public Mono deleteActions(Integer deleteAfterHours ){ + var dateAfter = Date.from(LocalDateTime.now().minusHours(deleteAfterHours).atZone(ZoneId.of("CET")).toInstant()); + return repository + .deleteAllByActionCreatedAtIsBefore(dateAfter) + .map(resp -> new Object()) + .onErrorResume(ProblemException.class,ex -> { + Logger.errorLog(null,"Delete all actions in cron job cannot be executed ", null); + return getError("Delete all actions after hours cannot be executed"); + }); + } + + /** + * + * @param resp List of ActionResponses + * @param saveInterval value will be part of the response action object. This value is set in the application properties. + * @return ActionsListResponse + */ + private ActionsListResponse toActionsListResponse(java.util.List actionResponses) { + var actionsListResponse = new ActionsListResponse(); + actionsListResponse.setActionsList(actionResponses); + actionsListResponse.setTotalCount(actionResponses.size()); + return actionsListResponse; + } + + /** + * + * @param actionsDao ActionsDao, return from the MongoDB repository query + * @param saveInterval value will be part of the response action object. This value is set in the application properties. + * @return action response object + */ + public ActionResponse toActionResponse(ActionsDao actionsDao, Integer saveInterval){ + return new ActionResponse() + .actionCreatedAt(actionsDao.getActionCreatedAt().toInstant().atOffset(ZoneOffset.ofHours(0))) + .saveInterval(saveInterval) + .action(actionsDao.getAction()); + } + + private ActionsDao toActionsDao(String userId, CreateActionRequest createActionRequest) { + var actionsDao = new ActionsDao(); + actionsDao.setUserId(userId); + actionsDao.setActionCreatedAt(new Date(createActionRequest.getActionCreatedAt().toEpochSecond()*1000)); + actionsDao.setAction(createActionRequest.getAction()); + return actionsDao; + } + + /** + * Build a problem exception with given message + * @param message will be detail part of the problem object + * @return Mono error with problem exception + */ + private Mono getError(String message) { + return Mono.error(ProblemException.builder() + .type(Problem.DEFAULT_TYPE) + .status(Status.BAD_REQUEST) + .title(HttpStatus.BAD_REQUEST.toString()) + .detail(message) + .build()); + } + +} diff --git a/app/src/main/java/org/onap/portal/history/util/IdTokenExchange.java b/app/src/main/java/org/onap/portal/history/util/IdTokenExchange.java new file mode 100644 index 0000000..82cc67a --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/util/IdTokenExchange.java @@ -0,0 +1,126 @@ +/* + * + * 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.history.util; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; + +import java.text.ParseException; + +import org.onap.portal.history.exception.ProblemException; +import org.springframework.web.server.ServerWebExchange; +import org.zalando.problem.Problem; +import org.zalando.problem.Status; +import reactor.core.publisher.Mono; + +/** + * Represents a function that handles the JWT identity token. + * Use this to check if the incoming requests are authorized to call the given endpoint + */ + +public final class IdTokenExchange { + + public static final String X_AUTH_IDENTITY_HEADER = "X-Auth-Identity"; + public static final String JWT_CLAIM_USERID = "sub"; + + private IdTokenExchange(){ + + } + + /** + * Extract the identity header from the given {@link ServerWebExchange}. + * @param exchange the ServerWebExchange that contains information about the incoming request + * @param xRequestId the id of the request to use in error log + * @return the identity header in the form of Bearer {@literal } + */ + private static Mono extractIdentityHeader(ServerWebExchange exchange, String xRequestId) { + return Mono.just(exchange.getRequest().getHeaders().getOrEmpty(X_AUTH_IDENTITY_HEADER)) + .map(headers -> headers.get(0)) + .onErrorResume(Exception.class, ex -> Mono.error(ProblemException.builder() + .type(Problem.DEFAULT_TYPE) + .status(Status.FORBIDDEN) + .title("Forbidden access") + .detail(X_AUTH_IDENTITY_HEADER + " is not set") + .build())); + } + + /** + * Extract the identity token from the given {@link ServerWebExchange}. + * @see OpenId Connect ID Token + * @param exchange the ServerWebExchange that contains information about the incoming request + * @param xRequestId the id of the request to use in error log + * @return the identity token that contains user roles + */ + private static Mono extractIdToken(ServerWebExchange exchange, String xRequestId) { + return extractIdentityHeader(exchange, xRequestId) + .map(identityHeader -> identityHeader.replace("Bearer ", "")); + } + + /** + * Extract the userId from the given {@link ServerWebExchange} + * @param exchange the ServerWebExchange that contains information about the incoming request + * @param xRequestId the id of the request to use in error log + * @return the id of the user + */ + public static Mono extractUserId(ServerWebExchange exchange,String xRequestId) { + return extractIdToken(exchange, xRequestId) + .flatMap(idToken -> extractUserClaim(idToken)); + } + + private static Mono extractUserClaim(String idToken) { + JWTClaimsSet jwtClaimSet; + try { + jwtClaimSet = JWTParser.parse(idToken).getJWTClaimsSet(); + } catch (ParseException e) { + return Mono.error(e); + } + return Mono.just(String.class.cast(jwtClaimSet.getClaim(JWT_CLAIM_USERID))); + } + + + /** + * Validate if given userId is same as extracted from the given {@link ServerWebExchange} + * @param userId from the path parameter of the REST call + * @param exchange the ServerWebExchange that contains information about the incoming request + * @param xRequestId the id of the request to use in error log + * @return empty Mono userId is the same as extracted from {@link ServerWebExchange} + * Forbidden userId is not the same as extracted from {@link ServerWebExchange} + */ + public static Mono validateUserId(String userId, ServerWebExchange exchange, String xRequestId){ + + return extractUserId(exchange, xRequestId) + .map(userSub -> userSub.equals(userId)) + .flatMap( match -> { + if (Boolean.TRUE.equals(match)) { + return Mono.empty(); + } else{ + Logger.errorLog(xRequestId,"Requested "+ userId + " did not match the JWT in the X-Auth-Identity header" , userId ); + return Mono.error(ProblemException.builder() + .type(Problem.DEFAULT_TYPE) + .status(Status.FORBIDDEN) + .title("Forbidden access") + .detail("UserId did not match with JWT in " + X_AUTH_IDENTITY_HEADER) + .build()); + } + }); + } +} diff --git a/app/src/main/java/org/onap/portal/history/util/Logger.java b/app/src/main/java/org/onap/portal/history/util/Logger.java new file mode 100644 index 0000000..4cb3420 --- /dev/null +++ b/app/src/main/java/org/onap/portal/history/util/Logger.java @@ -0,0 +1,64 @@ +/* + * + * 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.history.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; + +import java.net.URI; + +@Slf4j +public class Logger { + + private Logger(){} + + /** + * Write log to stdout for incoming request + * @param xRequestId from the request header + * @param methode http methode which is invoke + * @param path which is called be the request + */ + public static void requestLog(String xRequestId, HttpMethod methode, URI path) { + log.info("Portal-history - request - X-Request-Id {} {} {}", xRequestId, methode, path); + } + + /** + * Write log to stdout for the outgoing response + * @param xRequestId from the request header + * @param code http status of the response + */ + public static void responseLog(String xRequestId, HttpStatus code) { + log.info("Portal-history - response - X-Request-Id {} {}", xRequestId, code); + } + + /** + * Write error log to stdout + * @param xRequestId from the request header + * @param msg message which should be written + * @param id of the related object of the message + */ + public static void errorLog(String xRequestId, String msg, String id) { + log.info( + "Portal-history - error - X-Request-Id {} {} {} not found", xRequestId, msg, id); + } +} diff --git a/app/src/main/resources/application-local.yml b/app/src/main/resources/application-local.yml new file mode 100644 index 0000000..a908c1b --- /dev/null +++ b/app/src/main/resources/application-local.yml @@ -0,0 +1,42 @@ +server: + port: 9002 + address: 0.0.0.0 + +spring: + jackson: + serialization: + # needed for serializing objects of type object + FAIL_ON_EMPTY_BEANS: false + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: http://localhost:8080/auth/realms/ONAP/protocol/openid-connect/certs #Keycloak Endpoint + data: + mongodb: + database: portal_history + host: localhost + port: 27017 + username: root + password: password + +portal-history: + save-interval: 72 + delete-interval: 0 * * * * * + +management: + endpoints: + web: + exposure: + include: "*" + info: + build: + enabled: true + env: + enabled: true + git: + enabled: true + java: + enabled: true + + diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml new file mode 100644 index 0000000..3bfd624 --- /dev/null +++ b/app/src/main/resources/application.yml @@ -0,0 +1,40 @@ +server: + port: 9002 + address: 0.0.0.0 + +spring: + jackson: + serialization: + # needed for serializing objects of type object + FAIL_ON_EMPTY_BEANS: false + security: + oauth2: + resourceserver: + jwt: + jwk-set-uri: ${KEYCLOAK_URL}/auth/realms/${KEYCLOAK_REALM}/protocol/openid-connect/certs #Keycloak Endpoint + data: + mongodb: + database: ${PORTALHISTORY_DATABASE} + host: ${PORTALHISTORY_HOST} + port: ${PORTALHISTORY_PORT} + username: ${PORTALHISTORY_USERNAME} + password: ${PORTALHISTORY_PASSWORD} + +portal-history: + save-interval: 72 + delete-interval: 0 0 * * * * + +management: + endpoints: + web: + exposure: + include: "*" + info: + build: + enabled: true + env: + enabled: true + git: + enabled: true + java: + enabled: true diff --git a/app/src/main/resources/logback-spring.xml b/app/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f4ef0bf --- /dev/null +++ b/app/src/main/resources/logback-spring.xml @@ -0,0 +1,15 @@ + + + + + + + ${LOGBACK_LEVEL:-info} + + + + + + + + -- cgit 1.2.3-korg