Add basic security to query interface 23/123323/8
authorRenu Kumari <renu.kumari@bell.ca>
Tue, 17 Aug 2021 11:30:19 +0000 (07:30 -0400)
committerRenu Kumari <renu.kumari@bell.ca>
Fri, 20 Aug 2021 11:54:25 +0000 (07:54 -0400)
- 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

pom.xml
src/main/java/org/onap/cps/temporal/controller/rest/QueryController.java
src/main/java/org/onap/cps/temporal/controller/rest/QueryResponseFactory.java [deleted file]
src/main/java/org/onap/cps/temporal/controller/rest/config/WebSecurityConfig.java [new file with mode: 0644]
src/main/java/org/onap/cps/temporal/controller/rest/model/SortMapper.java
src/main/resources/application.yml
src/test/groovy/org/onap/cps/temporal/controller/rest/ControllerSecuritySpec.groovy [new file with mode: 0644]
src/test/groovy/org/onap/cps/temporal/controller/rest/QueryControllerSpec.groovy
src/test/groovy/org/onap/cps/temporal/domain/SearchCriteriaSpec.groovy
src/test/resources/application.yml

diff --git a/pom.xml b/pom.xml
index 2975f92..be5f72a 100755 (executable)
--- a/pom.xml
+++ b/pom.xml
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.spockframework</groupId>
             <artifactId>spock-core</artifactId>
index ab29e19..da1a9ea 100644 (file)
 
 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 (file)
index 6ac4759..0000000
+++ /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 (file)
index 0000000..647a0b0
--- /dev/null
@@ -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);
+    }
+}
index cd553eb..789284e 100644 (file)
@@ -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);
index 41eddf8..a3b1cd8 100755 (executable)
@@ -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 (file)
index 0000000..2ced672
--- /dev/null
@@ -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<NetworkData>([], 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
+    }
+
+}
index a18a134..7847b34 100644 (file)
@@ -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
index 3d6a354..32bc660 100644 (file)
@@ -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'
     }
 
index fce4a17..6765057 100644 (file)
@@ -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