From: Renu Kumari Date: Tue, 17 Aug 2021 11:30:19 +0000 (-0400) Subject: Add basic security to query interface X-Git-Tag: 1.0.0~6^2 X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=743380d1f171d4c0dd46dc0cd5b47d8ea93bea44;p=cps%2Fcps-temporal.git 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 Change-Id: I7e03ed9ccf983090ce514873b86fc9b2f851ed4f --- diff --git a/pom.xml b/pom.xml index 2975f92..be5f72a 100755 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-security + org.springframework.boot spring-boot-starter-actuator @@ -162,6 +166,11 @@ + + org.springframework.security + spring-security-test + test + org.spockframework spock-core 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 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 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 convertToAnchorDetails(final List 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 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 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 convertToAnchorDetails(final List 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: diff --git a/src/test/groovy/org/onap/cps/temporal/controller/rest/ControllerSecuritySpec.groovy b/src/test/groovy/org/onap/cps/temporal/controller/rest/ControllerSecuritySpec.groovy new file mode 100644 index 0000000..2ced672 --- /dev/null +++ b/src/test/groovy/org/onap/cps/temporal/controller/rest/ControllerSecuritySpec.groovy @@ -0,0 +1,99 @@ +/* + * ============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 org.onap.cps.temporal.controller.rest.config.WebSecurityConfig +import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapper +import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapperImpl +import org.onap.cps.temporal.controller.rest.model.AnchorHistory +import org.onap.cps.temporal.controller.rest.model.SortMapper +import org.onap.cps.temporal.domain.NetworkData +import org.onap.cps.temporal.service.NetworkDataService +import org.spockframework.spring.SpringBean +import org.spockframework.spring.StubBeans +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.annotation.Import +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.domain.SliceImpl +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.context.WebApplicationContext +import spock.lang.Shared +import spock.lang.Specification +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +@WebMvcTest(QueryController) +@Import([WebSecurityConfig, SortMapper, AnchorDetailsMapperImpl]) +class ControllerSecuritySpec extends Specification { + + @SpringBean + NetworkDataService mockNetworkDataService = Mock() { + searchNetworkData(_) >> new SliceImpl([], Pageable.ofSize(1), false) + } + + QueryController.QueryResponseFactory mockQueryResponseFactory = Mock() + + MockMvc mvc + + @Autowired + WebApplicationContext context + + @Shared + def testEndpoint = '/cps-temporal/api/v1/dataspaces/my-dataspace/anchors/my-anchor/history' + + def setup() { + mvc = MockMvcBuilders.webAppContextSetup(this.context).apply(springSecurity()).build(); + } + + def 'Get request with authentication: #scenario.'() { + given: 'authentication' + HttpHeaders httpHeaders = new HttpHeaders() + httpHeaders.setBasicAuth(username, password) + when: 'request is sent with authentication' + def response = mvc.perform(get(testEndpoint).headers(httpHeaders) + ).andReturn().response + then: 'expected http status is returned' + assert response.status == expectedHttpStatus.value() + where: + scenario | username | password || expectedHttpStatus + 'correct credentials' | 'testUser' | 'testPassword' || HttpStatus.OK + 'unknown username' | 'unknown-user' | 'password' || HttpStatus.UNAUTHORIZED + 'wrong password' | 'cpsuser' | 'wrong-password' || HttpStatus.UNAUTHORIZED + } + + def 'Get urls without authentication : #scenario.'() { + when: 'request is sent without authentication' + def response = mvc.perform(get(url) + ).andReturn().response + then: 'expected http status is returned' + assert response.status == expectedHttpStatus.value() + where: + scenario | url | expectedHttpStatus + 'permitted url' | '/swagger/openapi.yml' | HttpStatus.OK + 'not-permitted url' | testEndpoint | HttpStatus.UNAUTHORIZED + } + +} diff --git a/src/test/groovy/org/onap/cps/temporal/controller/rest/QueryControllerSpec.groovy b/src/test/groovy/org/onap/cps/temporal/controller/rest/QueryControllerSpec.groovy index a18a134..7847b34 100644 --- a/src/test/groovy/org/onap/cps/temporal/controller/rest/QueryControllerSpec.groovy +++ b/src/test/groovy/org/onap/cps/temporal/controller/rest/QueryControllerSpec.groovy @@ -21,8 +21,7 @@ package org.onap.cps.temporal.controller.rest import org.onap.cps.temporal.controller.utils.DateTimeUtility -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders - +import com.fasterxml.jackson.databind.ObjectMapper import java.time.OffsetDateTime import org.onap.cps.temporal.controller.rest.model.AnchorDetails import org.onap.cps.temporal.controller.rest.model.AnchorDetailsMapperImpl @@ -41,13 +40,14 @@ import org.springframework.data.domain.SliceImpl import org.springframework.data.domain.Sort import org.springframework.http.HttpStatus import org.springframework.http.MediaType +import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.web.servlet.MockMvc -import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper -import spock.lang.Shared import spock.lang.Specification +import spock.lang.Shared @WebMvcTest(QueryController) -@Import([SortMapper, QueryResponseFactory, AnchorDetailsMapperImpl]) +@Import([SortMapper, AnchorDetailsMapperImpl]) +@WithMockUser class QueryControllerSpec extends Specification { @SpringBean diff --git a/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy b/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy index 3d6a354..32bc660 100644 --- a/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy +++ b/src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy @@ -131,12 +131,12 @@ class SearchCriteriaSpec extends Specification { then: 'exception is thrown' def illegalArgumentException = thrown(IllegalArgumentException) def message = illegalArgumentException.getMessage(); - assert message.contains("sort") + assert message.contains('sort') assert message.contains(expectedExceptionMessage) where: scenario | sort | expectedExceptionMessage - 'null' | null | "null" - 'unsupported properties' | Sort.by(Sort.Direction.ASC, 'unsupported') | "Invalid sorting" + 'null' | null | 'null' + 'unsupported properties' | Sort.by(Sort.Direction.ASC, 'unsupported') | 'Invalid sorting' 'missing required sort' | Sort.by(Sort.Direction.ASC, 'anchor') | 'Missing mandatory sort' } diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index fce4a17..6765057 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -63,4 +63,11 @@ app: topic: cps.cfg-state-events query: response: - max-page-size: 20 \ No newline at end of file + max-page-size: 20 + +security: + # comma-separated uri patterns which do not require authorization + permit-uri: /manage/**,/swagger-ui/**,/swagger-resources/**,/swagger/openapi.yml + auth: + username: testUser + password: testPassword