summaryrefslogtreecommitdiffstats
path: root/src/main
diff options
context:
space:
mode:
authorRenu Kumari <renu.kumari@bell.ca>2021-08-17 07:30:19 -0400
committerRenu Kumari <renu.kumari@bell.ca>2021-08-20 07:54:25 -0400
commit743380d1f171d4c0dd46dc0cd5b47d8ea93bea44 (patch)
tree28797238bc83b03d6f99d495a8b351c0b67c4465 /src/main
parentea04c07ad990b5543766e95e234cae746bd1fbc1 (diff)
Add basic security to query interface
- Added WebSecurity configuration and corresponding test case - Updated existing test cases to handle spring security - Moved QueryResponseFactory to QueryController to avoid cyclic dependency Issue-ID: CPS-530 Signed-off-by: Renu Kumari <renu.kumari@bell.ca> Change-Id: I7e03ed9ccf983090ce514873b86fc9b2f851ed4f
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java166
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java170
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/config/WebSecurityConfig.java80
-rw-r--r--src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java2
-rwxr-xr-xsrc/main/resources/application.yml6
5 files changed, 248 insertions, 176 deletions
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java b/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
index ab29e19..da1a9ea 100644
--- a/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
@@ -20,22 +20,32 @@
package org.onap.cps.temporal.controller.rest;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
+
import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang3.StringUtils;
+import org.onap.cps.temporal.controller.rest.model.AnchorDetails;
+import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapper;
import org.onap.cps.temporal.controller.rest.model.AnchorHistory;
import org.onap.cps.temporal.controller.rest.model.SortMapper;
import org.onap.cps.temporal.controller.utils.DateTimeUtility;
import org.onap.cps.temporal.domain.NetworkData;
import org.onap.cps.temporal.domain.SearchCriteria;
import org.onap.cps.temporal.service.NetworkDataService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping("${rest.api.base-path}")
@@ -48,16 +58,18 @@ public class QueryController implements CpsTemporalQueryApi {
/**
* Constructor.
*
- * @param networkDataService networkDataService
- * @param sortMapper sortMapper
- * @param queryResponseFactory anchorHistoryResponseFactory
+ * @param networkDataService networkDataService
+ * @param sortMapper sortMapper
+ * @param anchorDetailsMapper anchorDetailsMapper
+ * @param basePath basePath
*/
public QueryController(final NetworkDataService networkDataService,
final SortMapper sortMapper,
- final QueryResponseFactory queryResponseFactory) {
+ final AnchorDetailsMapper anchorDetailsMapper,
+ @Value("${rest.api.base-path}") final String basePath) {
this.networkDataService = networkDataService;
this.sortMapper = sortMapper;
- this.queryResponseFactory = queryResponseFactory;
+ this.queryResponseFactory = new QueryResponseFactory(sortMapper, anchorDetailsMapper, basePath);
}
@Override
@@ -126,4 +138,148 @@ public class QueryController implements CpsTemporalQueryApi {
}
+ public static class QueryResponseFactory {
+
+ private SortMapper sortMapper;
+ private String basePath;
+ private AnchorDetailsMapper anchorDetailsMapper;
+
+ /**
+ * Constructor.
+ *
+ * @param sortMapper sortMapper
+ * @param anchorDetailsMapper anchorDetailsMapper
+ * @param basePath basePath
+ */
+ public QueryResponseFactory(final SortMapper sortMapper,
+ final AnchorDetailsMapper anchorDetailsMapper,
+ final String basePath) {
+ this.sortMapper = sortMapper;
+ this.anchorDetailsMapper = anchorDetailsMapper;
+ this.basePath = basePath;
+ }
+
+ /**
+ * Use search criteria and search result-set to create response.
+ *
+ * @param searchCriteria searchCriteria
+ * @param searchResult searchResult
+ * @return AnchorHistory
+ */
+ public AnchorHistory createAnchorsDataByFilterResponse(final SearchCriteria searchCriteria,
+ final Slice<NetworkData> searchResult) {
+
+ final var anchorHistory = new AnchorHistory();
+ if (searchResult.hasNext()) {
+ anchorHistory.setNextRecordsLink(
+ toRelativeLink(
+ getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.nextPageable())));
+ }
+ if (searchResult.hasPrevious()) {
+ anchorHistory.setPreviousRecordsLink(
+ toRelativeLink(
+ getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, searchResult.previousPageable())));
+ }
+ anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
+ return anchorHistory;
+ }
+
+ /**
+ * Use search criteria and search result-set to create response.
+ *
+ * @param searchCriteria searchCriteria
+ * @param searchResult searchResult
+ * @return AnchorHistory
+ */
+ public AnchorHistory createAnchorDataByNameResponse(final SearchCriteria searchCriteria,
+ final Slice<NetworkData> searchResult) {
+
+ final var anchorHistory = new AnchorHistory();
+ if (searchResult.hasNext()) {
+ anchorHistory.setNextRecordsLink(toRelativeLink(
+ getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.nextPageable())));
+ }
+ if (searchResult.hasPrevious()) {
+ anchorHistory.setPreviousRecordsLink(toRelativeLink(
+ getAbsoluteLinkForGetAnchorDataByName(searchCriteria, searchResult.previousPageable())));
+ }
+ anchorHistory.setRecords(convertToAnchorDetails(searchResult.getContent()));
+ return anchorHistory;
+ }
+
+ private List<AnchorDetails> convertToAnchorDetails(final List<NetworkData> networkDataList) {
+ return networkDataList.stream()
+ .map(networkData -> anchorDetailsMapper.toAnchorDetails(networkData))
+ .collect(Collectors.toList());
+ }
+
+ /*
+ Spring hateoas only provides absolute link. But in the microservices, relative links will be more appropriate
+ */
+ private String toRelativeLink(final String absoluteLink) {
+
+ /* Spring hateoas Issue:
+ It does replace the variable defined at the Controller level,
+ so we are removing the variable name and replace it with basePath.
+ https://github.com/spring-projects/spring-hateoas/issues/361
+ https://github.com/spring-projects/spring-hateoas/pull/1375
+ */
+ final int contextPathBeginIndex = absoluteLink.indexOf("rest.api.base-path%257D");
+ return basePath + absoluteLink.substring(contextPathBeginIndex + 23);
+ }
+
+ private String getAbsoluteLinkForGetAnchorDataByName(final SearchCriteria searchCriteria,
+ final Pageable pageable) {
+ final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorDataByName(
+ searchCriteria.getDataspaceName(),
+ searchCriteria.getAnchorName(),
+ DateTimeUtility.toString(searchCriteria.getObservedAfter()),
+ null,
+ DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
+ pageable.getPageNumber(), pageable.getPageSize(),
+ sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
+ .toUriComponentsBuilder();
+ addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
+ return encodePlusSign(uriComponentsBuilder.toUriString());
+ }
+
+ private String getAbsoluteLinkForGetAnchorsDataByFilter(final SearchCriteria searchCriteria,
+ final Pageable pageable) {
+ final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorsDataByFilter(
+ searchCriteria.getDataspaceName(),
+ searchCriteria.getSchemaSetName(),
+ DateTimeUtility.toString(searchCriteria.getObservedAfter()),
+ null,
+ DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
+ pageable.getPageNumber(), pageable.getPageSize(),
+ sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
+ .toUriComponentsBuilder();
+ addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
+ return encodePlusSign(uriComponentsBuilder.toUriString());
+ }
+
+ /*
+ Spring hateoas does double encoding when generting URI.
+ To avoid it in the case of simplePayloadFilter,
+ the 'simplePayloadFilter is being added explicitly to UriComponentsBuilder
+ */
+ private UriComponentsBuilder addSimplePayloadFilter(final UriComponentsBuilder uriComponentsBuilder,
+ final String simplePayloadFilter) {
+ if (simplePayloadFilter != null) {
+ uriComponentsBuilder.queryParam("simplePayloadFilter", simplePayloadFilter);
+ }
+ return uriComponentsBuilder;
+ }
+
+ /*
+ Spring hateoas does not encode '+' in the query param but it deccodes '+' as space.
+ Due to this inconsistency, API was failing to convert datetime with positive timezone.
+ The fix is done in the spring-hateoas 1.4 version but it is yet to release.
+ As a workaround, we are replacing all the '+' with '%2B'
+ https://github.com/spring-projects/spring-hateoas/issues/1485
+ */
+ private String encodePlusSign(final String link) {
+ return link.replace("+", "%2B");
+ }
+ }
}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java b/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java
deleted file mode 100644
index 6ac4759..0000000
--- a/src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * ============LICENSE_START=======================================================
- * Copyright (c) 2021 Bell Canada.
- * ================================================================================
- * 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
- * ============LICENSE_END=========================================================
- */
-
-package org.onap.cps.temporal.controller.rest;
-
-import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
-import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
-
-import java.util.List;
-import java.util.stream.Collectors;
-import org.onap.cps.temporal.controller.rest.model.AnchorDetails;
-import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapper;
-import org.onap.cps.temporal.controller.rest.model.AnchorHistory;
-import org.onap.cps.temporal.controller.rest.model.SortMapper;
-import org.onap.cps.temporal.controller.utils.DateTimeUtility;
-import org.onap.cps.temporal.domain.NetworkData;
-import org.onap.cps.temporal.domain.SearchCriteria;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.domain.Pageable;
-import org.springframework.data.domain.Slice;
-import org.springframework.stereotype.Component;
-import org.springframework.web.util.UriComponentsBuilder;
-
-@Component
-public class QueryResponseFactory {
-
- private SortMapper sortMapper;
- private String basePath;
- private AnchorDetailsMapper anchorDetailsMapper;
-
- /**
- * Constructor.
- *
- * @param sortMapper sortMapper
- * @param anchorDetailsMapper anchorDetailsMapper
- * @param basePath basePath
- */
- public QueryResponseFactory(final SortMapper sortMapper,
- final AnchorDetailsMapper anchorDetailsMapper,
- @Value("${rest.api.base-path}") final String basePath) {
- this.sortMapper = sortMapper;
- this.anchorDetailsMapper = anchorDetailsMapper;
- this.basePath = basePath;
- }
-
- AnchorHistory createAnchorsDataByFilterResponse(final SearchCriteria searchCriteria,
- final Slice<NetworkData> response) {
-
- final var anchorHistory = new AnchorHistory();
- if (response.hasNext()) {
- anchorHistory.setNextRecordsLink(
- toRelativeLink(getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, response.nextPageable())));
- }
- if (response.hasPrevious()) {
- anchorHistory.setPreviousRecordsLink(
- toRelativeLink(
- getAbsoluteLinkForGetAnchorsDataByFilter(searchCriteria, response.previousPageable())));
- }
- anchorHistory.setRecords(convertToAnchorDetails(response.getContent()));
- return anchorHistory;
- }
-
- AnchorHistory createAnchorDataByNameResponse(final SearchCriteria searchCriteria,
- final Slice<NetworkData> response) {
-
- final var anchorHistory = new AnchorHistory();
- if (response.hasNext()) {
- anchorHistory.setNextRecordsLink(toRelativeLink(
- getAbsoluteLinkForGetAnchorDataByName(searchCriteria, response.nextPageable())));
- }
- if (response.hasPrevious()) {
- anchorHistory.setPreviousRecordsLink(toRelativeLink(
- getAbsoluteLinkForGetAnchorDataByName(searchCriteria, response.previousPageable())));
- }
- anchorHistory.setRecords(convertToAnchorDetails(response.getContent()));
- return anchorHistory;
- }
-
- private List<AnchorDetails> convertToAnchorDetails(final List<NetworkData> networkDataList) {
- return networkDataList.stream()
- .map(networkData -> anchorDetailsMapper.toAnchorDetails(networkData))
- .collect(Collectors.toList());
- }
-
- /*
- Spring hateoas only provides absolute link. But in the microservices, relative links will be more appropriate
- */
- private String toRelativeLink(final String absoluteLink) {
-
- /* Spring hateoas Issue:
- It does replace the variable defined at the Controller level,
- so we are removing the variable name and replace it with basePath.
- https://github.com/spring-projects/spring-hateoas/issues/361
- https://github.com/spring-projects/spring-hateoas/pull/1375
- */
- final int contextPathBeginIndex = absoluteLink.indexOf("rest.api.base-path%257D");
- return basePath + absoluteLink.substring(contextPathBeginIndex + 23);
- }
-
- private String getAbsoluteLinkForGetAnchorDataByName(final SearchCriteria searchCriteria,
- final Pageable pageable) {
- final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorDataByName(
- searchCriteria.getDataspaceName(),
- searchCriteria.getAnchorName(),
- DateTimeUtility.toString(searchCriteria.getObservedAfter()),
- null,
- DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
- pageable.getPageNumber(), pageable.getPageSize(),
- sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
- .toUriComponentsBuilder();
- addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
- return encodePlusSign(uriComponentsBuilder.toUriString());
- }
-
- private String getAbsoluteLinkForGetAnchorsDataByFilter(final SearchCriteria searchCriteria,
- final Pageable pageable) {
- final var uriComponentsBuilder = linkTo(methodOn(QueryController.class).getAnchorsDataByFilter(
- searchCriteria.getDataspaceName(),
- searchCriteria.getSchemaSetName(),
- DateTimeUtility.toString(searchCriteria.getObservedAfter()),
- null,
- DateTimeUtility.toString(searchCriteria.getCreatedBefore()),
- pageable.getPageNumber(), pageable.getPageSize(),
- sortMapper.sortAsString(searchCriteria.getPageable().getSort())))
- .toUriComponentsBuilder();
- addSimplePayloadFilter(uriComponentsBuilder, searchCriteria.getSimplePayloadFilter());
- return encodePlusSign(uriComponentsBuilder.toUriString());
- }
-
- /*
- Spring hateoas does double encoding when generting URI.
- To avoid it in the case of simplePayloadFilter,
- the 'simplePayloadFilter is being added explicitly to UriComponentsBuilder
- */
- private UriComponentsBuilder addSimplePayloadFilter(final UriComponentsBuilder uriComponentsBuilder,
- final String simplePayloadFilter) {
- if (simplePayloadFilter != null) {
- uriComponentsBuilder.queryParam("simplePayloadFilter", simplePayloadFilter);
- }
- return uriComponentsBuilder;
- }
-
- /*
- Spring hateoas does not encode '+' in the query param but it deccodes '+' as space.
- Due to this inconsistency, API was failing to convert datetime with positive timezone.
- The fix is done in the spring-hateoas 1.4 version but it is yet to release.
- As a workaround, we are replacing all the '+' with '%2B'
- https://github.com/spring-projects/spring-hateoas/issues/1485
- */
- private String encodePlusSign(final String link) {
- return link.replace("+", "%2B");
- }
-}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/config/WebSecurityConfig.java b/src/main/java/org/onap/cps/temporal/controller/rest/config/WebSecurityConfig.java
new file mode 100644
index 0000000..647a0b0
--- /dev/null
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/config/WebSecurityConfig.java
@@ -0,0 +1,80 @@
+/*
+ * ============LICENSE_START=======================================================
+ * Copyright (c) 2021 Bell Canada.
+ * ================================================================================
+ * 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
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.temporal.controller.rest.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * Configuration class to implement application security. It enforces Basic Authentication access control.
+ */
+@Configuration
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ private static final String USER_ROLE = "USER";
+
+ private final String username;
+ private final String password;
+ private final String[] permitUris;
+
+ /**
+ * Constructor. Accepts parameters from configuration.
+ *
+ * @param permitUris comma-separated list of uri patterns for endpoints permitted
+ * @param username username
+ * @param password password
+ */
+ public WebSecurityConfig(
+ @Autowired @Value("${security.permit-uri}") final String permitUris,
+ @Autowired @Value("${security.auth.username}") final String username,
+ @Autowired @Value("${security.auth.password}") final String password
+ ) {
+ super();
+ this.permitUris =
+ permitUris.isEmpty() ? new String[]{"/swagger/openapi.yml"} : permitUris.split("\\s{0,9},\\s{0,9}");
+ this.username = username;
+ this.password = password;
+ }
+
+ @Override
+ // The team decided to disable default CSRF Spring protection and not implement CSRF tokens validation.
+ // CPS is a stateless REST API that is not as vulnerable to CSRF attacks as web applications running in
+ // web browsers are. CPS does not manage sessions, each request requires the authentication token in the header.
+ // See https://docs.spring.io/spring-security/site/docs/5.3.8.RELEASE/reference/html5/#csrf
+ @SuppressWarnings("squid:S4502")
+ protected void configure(final HttpSecurity http) throws Exception {
+ http
+ .csrf().disable()
+ .authorizeRequests()
+ .antMatchers(permitUris).permitAll()
+ .anyRequest().authenticated()
+ .and().httpBasic();
+ }
+
+ @Override
+ protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
+ auth.inMemoryAuthentication().withUser(username).password("{noop}" + password).roles(USER_ROLE);
+ }
+}
diff --git a/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java b/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
index cd553eb..789284e 100644
--- a/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
+++ b/src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
@@ -65,7 +65,7 @@ public class SortMapper {
for (final String eachSortAsString : sortingOrderAsString) {
final String[] eachSortDetail = eachSortAsString.split(FIELD_DIRECTION_SEPARATOR);
final var direction = Direction.fromString(eachSortDetail[1]);
- final var fieldName = eachSortDetail[0];
+ final String fieldName = eachSortDetail[0];
sortOrder.add(new Order(direction, fieldName));
}
return Sort.by(sortOrder);
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 41eddf8..a3b1cd8 100755
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -68,6 +68,12 @@ springdoc:
urls:
- name: query
url: /swagger/openapi.yml
+security:
+ # comma-separated uri patterns which do not require authorization
+ permit-uri: /manage/**,/swagger-ui/**,/swagger-resources/**,/swagger/openapi.yml
+ auth:
+ username: ${APP_USERNAME}
+ password: ${APP_PASSWORD}
# Actuator
management: