diff options
author | Renu Kumari <renu.kumari@bell.ca> | 2021-08-17 07:30:19 -0400 |
---|---|---|
committer | Renu Kumari <renu.kumari@bell.ca> | 2021-08-20 07:54:25 -0400 |
commit | 743380d1f171d4c0dd46dc0cd5b47d8ea93bea44 (patch) | |
tree | 28797238bc83b03d6f99d495a8b351c0b67c4465 /src/main/java | |
parent | ea04c07ad990b5543766e95e234cae746bd1fbc1 (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/java')
4 files changed, 242 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); |