Add basic authentication security 73/118773/13
authorBruno Sakoto <bruno.sakoto@bell.ca>
Wed, 3 Mar 2021 23:27:54 +0000 (18:27 -0500)
committerRishi Chail <rishi.chail@est.tech>
Fri, 5 Mar 2021 08:56:30 +0000 (08:56 +0000)
Endpoints exposed outside from the cluster require
basic authentication except actuator health and info endpoints.

Default user credentials are embedded in the application,
they can be overridden with system environment properties.

Issue-ID: CPS-175
Signed-off-by: Bruno Sakoto <bruno.sakoto@bell.ca>
Change-Id: I3dfa0e49e5f4538c923e6bbe9bef976d30359fe6

cps-rest/pom.xml
cps-rest/src/main/java/org/onap/cps/config/WebSecurityConfig.java [new file with mode: 0644]
cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy
cps-rest/src/test/groovy/org/onap/cps/rest/controller/RestControllerSpecification.groovy [new file with mode: 0644]
cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy
csit/tests/cps-admin/cps-admin.robot
csit/tests/cps-data/cps-data.robot
docker-compose/docker-compose.yml

index 9264c68..43875e8 100755 (executable)
@@ -1,3 +1,22 @@
+<!--
+  ============LICENSE_START=======================================================
+  Copyright (c) 2020 Linux Foundation.
+  Modifications 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.
+  ============LICENSE_END=========================================================
+-->
+
 <project xmlns="http://maven.apache.org/POM/4.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-jetty</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
         <dependency>
             <groupId>io.swagger.core.v3</groupId>
             <artifactId>swagger-annotations</artifactId>
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/cps-rest/src/main/java/org/onap/cps/config/WebSecurityConfig.java b/cps-rest/src/main/java/org/onap/cps/config/WebSecurityConfig.java
new file mode 100644 (file)
index 0000000..943e02c
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * ============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.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.cps.config;
+
+import org.apache.commons.lang3.StringUtils;
+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.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * Configuration class to implement application security.
+ * It enforces Basic Authentication access control.
+ */
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private static final String ACTUATOR_HEALTH_PATTERN = "/manage/health/**";
+    private static final String ACTUATOR_INFO_PATTERN = "/manage/info";
+    private static final String DEFAULT_USER_NAME = "cpsuser";
+    private static final String DEFAULT_USER_PASSWORD = "cpsr0cks!";
+    private static final String USER_NAME =
+            StringUtils.defaultIfBlank(System.getenv("CPS_USERNAME"), DEFAULT_USER_NAME);
+    private static final String USER_PASSWORD =
+            StringUtils.defaultIfBlank(System.getenv("CPS_PASSWORD"), DEFAULT_USER_PASSWORD);
+    private static final String USER_ROLE = "USER";
+
+    @Override
+    protected void configure(final HttpSecurity http) throws Exception {
+        http
+                .csrf().disable()
+                .authorizeRequests()
+                .antMatchers(ACTUATOR_HEALTH_PATTERN, ACTUATOR_INFO_PATTERN).permitAll()
+                .anyRequest().authenticated()
+                .and().httpBasic();
+    }
+
+    @Override
+    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
+        auth.inMemoryAuthentication().withUser(USER_NAME).password("{noop}" + USER_PASSWORD).roles(USER_ROLE);
+    }
+
+}
index ca99743..f381938 100755 (executable)
@@ -1,7 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2020 Pantheon.tech
- *  Modifications Copyright (C) 2020 Bell Canada. All rights reserved.
+ *  Modifications Copyright (C) 2020, 2021 Bell Canada. All rights reserved.
  *  Copyright (C) 2021 Nordix Foundation
  *  ================================================================================
  *  Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,10 +28,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 
 import org.modelmapper.ModelMapper
-import org.onap.cps.api.CpsQueryService
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
+import org.onap.cps.api.CpsQueryService
 import org.onap.cps.spi.exceptions.DataspaceAlreadyDefinedException
 import org.onap.cps.spi.exceptions.SchemaSetInUseException
 import org.onap.cps.spi.model.Anchor
@@ -46,11 +46,10 @@ import org.springframework.mock.web.MockMultipartFile
 import org.springframework.test.web.servlet.MockMvc
 import org.springframework.util.LinkedMultiValueMap
 import org.springframework.util.MultiValueMap
-import spock.lang.Specification
 import spock.lang.Unroll
 
 @WebMvcTest
-class AdminRestControllerSpec extends Specification {
+class AdminRestControllerSpec extends RestControllerSpecification {
 
     @SpringBean
     CpsModuleService mockCpsModuleService = Mock()
@@ -83,8 +82,12 @@ class AdminRestControllerSpec extends Specification {
         given: 'an endpoint'
             def createDataspaceEndpoint = "$basePath/v1/dataspaces";
         when: 'post is invoked'
-            def response = mvc.perform(
-                    post(createDataspaceEndpoint).param('dataspace-name', dataspaceName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            post(createDataspaceEndpoint)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('dataspace-name', dataspaceName))
+                            .andReturn().response
         then: 'service method is invoked with expected parameters'
             1 * mockCpsAdminService.createDataspace(dataspaceName)
         and: 'dataspace is create successfully'
@@ -98,7 +101,12 @@ class AdminRestControllerSpec extends Specification {
             def thrownException = new DataspaceAlreadyDefinedException("", new RuntimeException())
             mockCpsAdminService.createDataspace(dataspaceName) >> { throw thrownException }
         when: 'post is invoked'
-            def response = mvc.perform(post(createDataspaceEndpoint).param('dataspace-name', dataspaceName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            post(createDataspaceEndpoint)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('dataspace-name', dataspaceName))
+                            .andReturn().response
         then: 'dataspace creation fails'
             response.status == HttpStatus.BAD_REQUEST.value()
     }
@@ -110,8 +118,13 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
         when: 'file uploaded with schema set create request'
-            def response = mvc.perform(multipart(schemaSetEndpoint)
-                    .file(multipartFile).param('schema-set-name', schemaSetName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            multipart(schemaSetEndpoint)
+                                    .file(multipartFile)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('schema-set-name', schemaSetName))
+                            .andReturn().response
         then: 'associated service method is invoked with expected parameters'
             1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _) >>
                     { args -> yangResourceMapCapture = args[2] }
@@ -127,8 +140,13 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
         when: 'file uploaded with schema set create request'
-            def response = mvc.perform(multipart(schemaSetEndpoint)
-                    .file(multipartFile).param('schema-set-name', schemaSetName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            multipart(schemaSetEndpoint)
+                                    .file(multipartFile)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('schema-set-name', schemaSetName))
+                            .andReturn().response
         then: 'associated service method is invoked with expected parameters'
             1 * mockCpsModuleService.createSchemaSet(dataspaceName, schemaSetName, _) >>
                     { args -> yangResourceMapCapture = args[2] }
@@ -143,8 +161,13 @@ class AdminRestControllerSpec extends Specification {
         given: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
         when: 'zip archive having #caseDescriptor is uploaded with create schema set request'
-            def response = mvc.perform(multipart(schemaSetEndpoint)
-                    .file(multipartFile).param('schema-set-name', schemaSetName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            multipart(schemaSetEndpoint)
+                                    .file(multipartFile)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('schema-set-name', schemaSetName))
+                            .andReturn().response
         then: 'create schema set rejected'
             response.status == HttpStatus.BAD_REQUEST.value()
         where: 'following cases are tested'
@@ -159,8 +182,13 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
         when: 'file uploaded with schema set create request'
-            def response = mvc.perform(multipart(schemaSetEndpoint)
-                    .file(multipartFile).param('schema-set-name', schemaSetName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            multipart(schemaSetEndpoint)
+                                    .file(multipartFile)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('schema-set-name', schemaSetName))
+                            .andReturn().response
         then: 'create schema set rejected'
             response.status == HttpStatus.BAD_REQUEST.value()
     }
@@ -171,8 +199,13 @@ class AdminRestControllerSpec extends Specification {
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets"
         when: 'file uploaded with schema set create request'
             def multipartFile = createMultipartFileForIOException(fileType)
-            def response = mvc.perform(multipart(schemaSetEndpoint)
-                    .file(multipartFile).param('schema-set-name', schemaSetName)).andReturn().response
+            def response =
+                    mvc.perform(
+                            multipart(schemaSetEndpoint)
+                                    .file(multipartFile)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('schema-set-name', schemaSetName))
+                            .andReturn().response
         then: 'the error response returned indicating internal server error occurrence'
             response.status == HttpStatus.INTERNAL_SERVER_ERROR.value()
         where: 'following file types are used'
@@ -183,7 +216,9 @@ class AdminRestControllerSpec extends Specification {
         given: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
         when: 'delete schema set endpoint is invoked'
-            def response = mvc.perform(delete(schemaSetEndpoint)).andReturn().response
+            def response =
+                    mvc.perform(delete(schemaSetEndpoint).header("Authorization", getAuthorizationHeader()))
+                            .andReturn().response
         then: 'associated service method is invoked with expected parameters'
             1 * mockCpsModuleService.deleteSchemaSet(dataspaceName, schemaSetName, CASCADE_DELETE_PROHIBITED)
         and: 'response code indicates success'
@@ -198,7 +233,9 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
         when: 'delete schema set endpoint is invoked'
-            def response = mvc.perform(delete(schemaSetEndpoint)).andReturn().response
+            def response =
+                    mvc.perform(delete(schemaSetEndpoint).header("Authorization", getAuthorizationHeader()))
+                            .andReturn().response
         then: 'schema set deletion fails with conflict response code'
             response.status == HttpStatus.CONFLICT.value()
     }
@@ -210,7 +247,9 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def schemaSetEndpoint = "$basePath/v1/dataspaces/$dataspaceName/schema-sets/$schemaSetName"
         when: 'get schema set API is invoked'
-            def response = mvc.perform(get(schemaSetEndpoint)).andReturn().response
+            def response =
+                    mvc.perform(get(schemaSetEndpoint).header("Authorization", getAuthorizationHeader()))
+                            .andReturn().response
         then: 'the correct schema set is returned'
             response.status == HttpStatus.OK.value()
             response.getContentAsString().contains(schemaSetName)
@@ -224,8 +263,12 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors"
         when: 'post is invoked'
-            def response = mvc.perform(post(anchorEndpoint).contentType(MediaType.APPLICATION_JSON)
-                    .params(requestParams as MultiValueMap)).andReturn().response
+            def response =
+                    mvc.perform(
+                            post(anchorEndpoint).contentType(MediaType.APPLICATION_JSON)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .params(requestParams as MultiValueMap))
+                            .andReturn().response
         then: 'anchor is created successfully'
             1 * mockCpsAdminService.createAnchor(dataspaceName, schemaSetName, anchorName)
             response.status == HttpStatus.CREATED.value()
@@ -238,7 +281,9 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors"
         when: 'get all anchors API is invoked'
-            def response = mvc.perform(get(anchorEndpoint)).andReturn().response
+            def response =
+                    mvc.perform(get(anchorEndpoint).header("Authorization", getAuthorizationHeader()))
+                            .andReturn().response
         then: 'the correct anchor is returned'
             response.status == HttpStatus.OK.value()
             response.getContentAsString().contains(anchorName)
@@ -250,8 +295,9 @@ class AdminRestControllerSpec extends Specification {
         and: 'an endpoint'
             def anchorEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName"
         when: 'get anchor API is invoked'
-            def response = mvc.perform(get(anchorEndpoint))
-                    .andReturn().response
+            def response =
+                    mvc.perform(get(anchorEndpoint).header("Authorization", getAuthorizationHeader()))
+                            .andReturn().response
             def responseContent = response.getContentAsString()
         then: 'the correct anchor is returned'
             response.status == HttpStatus.OK.value()
index b9b680d..ef834a7 100755 (executable)
@@ -2,6 +2,7 @@
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Nordix Foundation
  *  Modifications Copyright (C) 2021 Pantheon.tech
+ *  Modifications 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.
@@ -28,10 +29,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put
 
 import org.modelmapper.ModelMapper
-import org.onap.cps.api.CpsQueryService
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
+import org.onap.cps.api.CpsQueryService
 import org.onap.cps.spi.exceptions.AnchorNotFoundException
 import org.onap.cps.spi.exceptions.DataNodeNotFoundException
 import org.onap.cps.spi.exceptions.DataspaceNotFoundException
@@ -45,11 +46,10 @@ import org.springframework.http.HttpStatus
 import org.springframework.http.MediaType
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Shared
-import spock.lang.Specification
 import spock.lang.Unroll
 
 @WebMvcTest
-class DataRestControllerSpec extends Specification {
+class DataRestControllerSpec extends RestControllerSpecification {
 
     @SpringBean
     CpsDataService mockCpsDataService = Mock()
@@ -93,9 +93,12 @@ class DataRestControllerSpec extends Specification {
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
             def json = 'some json (this is not validated)'
         when: 'post is invoked with datanode endpoint and json'
-            def response = mvc.perform(
-                    post(endpoint).contentType(MediaType.APPLICATION_JSON).content(json)
-            ).andReturn().response
+            def response =
+                    mvc.perform(
+                            post(endpoint)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .contentType(MediaType.APPLICATION_JSON).content(json))
+                            .andReturn().response
         then: 'a created response is returned'
             response.status == HttpStatus.CREATED.value()
         then: 'the java API was called with the correct parameters'
@@ -109,9 +112,9 @@ class DataRestControllerSpec extends Specification {
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, OMIT_DESCENDANTS) >> dataNodeWithLeavesNoChildren
         when: 'get request is performed through REST API'
-            def response = mvc.perform(
-                    get(endpoint).param('xpath', xpath)
-            ).andReturn().response
+            def response =
+                    mvc.perform(get(endpoint).header("Authorization", getAuthorizationHeader()).param('xpath', xpath))
+                            .andReturn().response
         then: 'a success response is returned'
             response.status == HttpStatus.OK.value()
         and: 'response contains expected leaf and value'
@@ -127,10 +130,13 @@ class DataRestControllerSpec extends Specification {
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, expectedCpsDataServiceOption) >> dataNode
         when: 'get request is performed through REST API'
-            def response = mvc.perform(get(endpoint)
-                    .param('xpath', xpath)
-                    .param('include-descendants', includeDescendantsOption))
-                    .andReturn().response
+            def response =
+                    mvc.perform(
+                            get(endpoint)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('xpath', xpath)
+                                    .param('include-descendants', includeDescendantsOption))
+                            .andReturn().response
         then: 'a success response is returned'
             response.status == HttpStatus.OK.value()
         and: 'the response contains child is #expectChildInResponse'
@@ -148,9 +154,9 @@ class DataRestControllerSpec extends Specification {
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/node"
             mockCpsDataService.getDataNode(dataspaceName, anchorName, xpath, _) >> { throw exception }
         when: 'get request is performed through REST API'
-            def response = mvc.perform(
-                    get(endpoint).param("xpath", xpath)
-            ).andReturn().response
+            def response =
+                    mvc.perform(get(endpoint).header("Authorization", getAuthorizationHeader()).param("xpath", xpath))
+                            .andReturn().response
         then: 'a success response is returned'
             response.status == httpStatus.value()
         where:
@@ -167,12 +173,14 @@ class DataRestControllerSpec extends Specification {
             def jsonData = 'json data'
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
         when: 'patch request is performed'
-            def response = mvc.perform(
-                    patch(endpoint)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(jsonData)
-                            .param('xpath', xpath)
-            ).andReturn().response
+            def response =
+                    mvc.perform(
+                            patch(endpoint)
+                                    .contentType(MediaType.APPLICATION_JSON)
+                                    .content(jsonData)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('xpath', xpath)
+                    ).andReturn().response
         then: 'the service method is invoked with expected parameters'
             1 * mockCpsDataService.updateNodeLeaves(dataspaceName, anchorName, xpathServiceParameter, jsonData)
         and: 'response status indicates success'
@@ -189,12 +197,14 @@ class DataRestControllerSpec extends Specification {
             def jsonData = 'json data'
             def endpoint = "$dataNodeBaseEndpoint/anchors/$anchorName/nodes"
         when: 'put request is performed'
-            def response = mvc.perform(
-                    put(endpoint)
-                            .contentType(MediaType.APPLICATION_JSON)
-                            .content(jsonData)
-                            .param('xpath', xpath)
-            ).andReturn().response
+            def response =
+                    mvc.perform(
+                            put(endpoint)
+                                    .contentType(MediaType.APPLICATION_JSON)
+                                    .content(jsonData)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('xpath', xpath))
+                            .andReturn().response
         then: 'the service method is invoked with expected parameters'
             1 * mockCpsDataService.replaceNodeTree(dataspaceName, anchorName, xpathServiceParameter, jsonData)
         and: 'response status indicates success'
index 0927c9d..907528a 100644 (file)
@@ -1,6 +1,7 @@
 /*
  *  ============LICENSE_START=======================================================
  *  Copyright (C) 2021 Nordix Foundation
+ *  Modifications 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.
 
 package org.onap.cps.rest.controller
 
-
 import com.google.gson.Gson
 import org.modelmapper.ModelMapper
 import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsQueryService
-import org.onap.cps.spi.model.DataNode
 import org.onap.cps.spi.model.DataNodeBuilder
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
@@ -34,8 +33,6 @@ import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
 import org.springframework.http.HttpStatus
 import org.springframework.test.web.servlet.MockMvc
-import spock.lang.Shared
-import spock.lang.Specification
 import spock.lang.Unroll
 
 import static org.onap.cps.spi.FetchDescendantsOption.INCLUDE_ALL_DESCENDANTS
@@ -43,7 +40,7 @@ import static org.onap.cps.spi.FetchDescendantsOption.OMIT_DESCENDANTS
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 
 @WebMvcTest
-class QueryRestControllerSpec extends Specification {
+class QueryRestControllerSpec extends RestControllerSpecification {
 
     @SpringBean
     CpsDataService mockCpsDataService = Mock()
@@ -77,10 +74,13 @@ class QueryRestControllerSpec extends Specification {
         and: 'the query endpoint'
             def dataNodeEndpoint = "$basePath/v1/dataspaces/$dataspaceName/anchors/$anchorName/nodes/query"
         when: 'query data nodes API is invoked'
-            def response = mvc.perform(get(dataNodeEndpoint)
-                    .param('cps-path', cpsPath)
-                    .param('include-descendants', includeDescendantsOption))
-                    .andReturn().response
+            def response =
+                    mvc.perform(
+                            get(dataNodeEndpoint)
+                                    .header("Authorization", getAuthorizationHeader())
+                                    .param('cps-path', cpsPath)
+                                    .param('include-descendants', includeDescendantsOption))
+                            .andReturn().response
         then: 'the response contains the the datanode in json format'
             response.status == HttpStatus.OK.value()
             response.getContentAsString().contains(new Gson().toJson(dataNode))
diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/RestControllerSpecification.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/RestControllerSpecification.groovy
new file mode 100644 (file)
index 0000000..a700ea2
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * ============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.
+ * ============LICENSE_END=========================================================
+*/
+
+package org.onap.cps.rest.controller
+
+import spock.lang.Specification
+
+/**
+ * Abstract class for all rest controller specifications.
+ */
+abstract class RestControllerSpecification extends Specification {
+
+    def authorizationHeader = 'Basic Y3BzdXNlcjpjcHNyMGNrcyE='
+
+    def getAuthorizationHeader() {
+        return authorizationHeader
+    }
+
+}
\ No newline at end of file
index 4e10e2c..5ddc9d9 100644 (file)
@@ -2,6 +2,7 @@
  * ============LICENSE_START=======================================================
  *  Copyright (C) 2020 Pantheon.tech
  *  Copyright (C) 2021 Nordix Foundation
+ *  Modifications 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.
@@ -26,6 +27,7 @@ import org.onap.cps.api.CpsAdminService
 import org.onap.cps.api.CpsDataService
 import org.onap.cps.api.CpsModuleService
 import org.onap.cps.api.CpsQueryService
+import org.onap.cps.rest.controller.RestControllerSpecification
 import org.onap.cps.spi.exceptions.AnchorAlreadyDefinedException
 import org.onap.cps.spi.exceptions.CpsException
 import org.onap.cps.spi.exceptions.DataInUseException
@@ -40,17 +42,17 @@ import org.springframework.beans.factory.annotation.Value
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
 import org.springframework.test.web.servlet.MockMvc
 import spock.lang.Shared
-import spock.lang.Specification
 import spock.lang.Unroll
 
 import static org.springframework.http.HttpStatus.BAD_REQUEST
 import static org.springframework.http.HttpStatus.CONFLICT
 import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR
 import static org.springframework.http.HttpStatus.NOT_FOUND
+import static org.springframework.http.HttpStatus.UNAUTHORIZED
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 
 @WebMvcTest
-class CpsRestExceptionHandlerSpec extends Specification {
+class CpsRestExceptionHandlerSpec extends RestControllerSpecification {
 
     @SpringBean
     CpsAdminService mockCpsAdminService = Mock()
@@ -164,6 +166,25 @@ class CpsRestExceptionHandlerSpec extends Specification {
                                 new SchemaSetInUseException(dataspaceName, existingObjectName)]
     }
 
+    def 'Get request without authentication is not authorized'() {
+        when: 'request is sent without authentication'
+            def response =
+                    mvc.perform(get("$basePath/v1/dataspaces/dataspace-name/anchors")).andReturn().response
+        then: 'HTTP Unauthorized status code is returned'
+            assert UNAUTHORIZED.value() == response.status
+    }
+
+    def 'Get request with invalid authentication is not authorized'() {
+        when: 'request is sent with invalid authentication'
+            def response =
+                    mvc.perform(
+                            get("$basePath/v1/dataspaces/dataspace-name/anchors")
+                                    .header("Authorization", 'Basic invalid auth'))
+                            .andReturn().response
+        then: 'HTTP Unauthorized status code is returned'
+            assert UNAUTHORIZED.value() == response.status
+    }
+
     /*
      * NB. The test uses 'get JSON by id' endpoint and associated service method invocation
      * to test the exception handling. The endpoint chosen is not a subject of test.
@@ -174,7 +195,10 @@ class CpsRestExceptionHandlerSpec extends Specification {
     }
 
     def performTestRequest() {
-        return mvc.perform(get("$basePath/v1/dataspaces/dataspace-name/anchors")).andReturn().response
+        return mvc.perform(
+                get("$basePath/v1/dataspaces/dataspace-name/anchors")
+                        .header("Authorization", getAuthorizationHeader()))
+                .andReturn().response
     }
 
     void assertTestResponse(response, expectedStatus,
index 446a59b..0e07f38 100644 (file)
@@ -9,6 +9,7 @@ Suite Setup           Create Session      CPS_HOST    ${CPS_HOST}
 
 *** Variables ***
 
+${auth}                 Basic Y3BzdXNlcjpjcHNyMGNrcyE=
 ${basePath}             /cps/api
 ${dataspaceName}        CSIT-Dataspace
 ${schemaSetName}        CSIT-SchemaSet
@@ -18,7 +19,8 @@ ${anchorName}           CSIT-Anchor
 Create Dataspace
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces
     ${params}=          Create Dictionary   dataspace-name=${dataspaceName}
-    ${response}=        POST On Session     CPS_HOST   ${uri}   params=${params}
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        POST On Session     CPS_HOST   ${uri}   params=${params}   headers=${headers}
     Should Be Equal As Strings              ${response.status_code}   201
 
 Create Schema Set from YANG file
@@ -27,7 +29,8 @@ Create Schema Set from YANG file
     ${fileData}=        Get Binary File     ${DATADIR}${/}test-tree.yang
     ${fileTuple}=       Create List         test.yang   ${fileData}   application/zip
     &{files}=           Create Dictionary   file=${fileTuple}
-    ${response}=        POST On Session     CPS_HOST   ${uri}   files=${files}   params=${params}
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        POST On Session     CPS_HOST   ${uri}   files=${files}   params=${params}   headers=${headers}
     Should Be Equal As Strings              ${response.status_code}   201
 
 Create Schema Set from ZIP file
@@ -36,12 +39,14 @@ Create Schema Set from ZIP file
     ${fileData}=        Get Binary File     ${DATADIR}${/}yang-resources.zip
     ${fileTuple}=       Create List         test.zip   ${fileData}   application/zip
     &{files}=           Create Dictionary   file=${fileTuple}
-    ${response}=        POST On Session     CPS_HOST   ${uri}   files=${files}   params=${params}
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        POST On Session     CPS_HOST   ${uri}   files=${files}   params=${params}   headers=${headers}
     Should Be Equal As Strings              ${response.status_code}   201
 
 Get Schema Set info
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/schema-sets/${schemaSetName}
-    ${response}=        Get On Session      CPS_HOST   ${uri}   expected_status=200
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        Get On Session      CPS_HOST   ${uri}   headers=${headers}   expected_status=200
     ${responseJson}=    Set Variable        ${response.json()}
     Should Be Equal As Strings              ${responseJson['name']}   ${schemaSetName}
     Should Be Equal As Strings              ${responseJson['dataspaceName']}   ${dataspaceName}
@@ -49,5 +54,6 @@ Get Schema Set info
 Create Anchor
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/anchors
     ${params}=          Create Dictionary   schema-set-name=${schemaSetName}   anchor-name=${anchorName}
-    ${response}=        POST On Session     CPS_HOST   ${uri}   params=${params}
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        POST On Session     CPS_HOST   ${uri}   params=${params}   headers=${headers}
     Should Be Equal As Strings              ${response.status_code}   201
\ No newline at end of file
index 8b0202b..ff1e8d0 100644 (file)
@@ -9,6 +9,7 @@ Suite Setup           Create Session      CPS_HOST    ${CPS_HOST}
 
 *** Variables ***
 
+${auth}                 Basic Y3BzdXNlcjpjcHNyMGNrcyE=
 ${basePath}             /cps/api
 ${dataspaceName}        CSIT-Dataspace
 ${anchorName}           CSIT-Anchor
@@ -16,7 +17,7 @@ ${anchorName}           CSIT-Anchor
 *** Test Cases ***
 Create Data Node
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/anchors/${anchorName}/nodes
-    ${headers}          Create Dictionary   Content-Type=application/json
+    ${headers}          Create Dictionary   Content-Type=application/json   Authorization=${auth}
     ${jsonData}=        Get Binary File     ${DATADIR}${/}test-tree.json
     ${response}=        POST On Session     CPS_HOST   ${uri}   headers=${headers}   data=${jsonData}
     Should Be Equal As Strings              ${response.status_code}   201
@@ -24,7 +25,8 @@ Create Data Node
 Get Data Node by XPath
     ${uri}=             Set Variable        ${basePath}/v1/dataspaces/${dataspaceName}/anchors/${anchorName}/node
     ${params}=          Create Dictionary   xpath=/test-tree/branch[@name='Left']/nest
-    ${response}=        Get On Session      CPS_HOST   ${uri}   params=${params}   expected_status=200
+    ${headers}=         Create Dictionary   Authorization=${auth}
+    ${response}=        Get On Session      CPS_HOST   ${uri}   params=${params}   headers=${headers}   expected_status=200
     ${responseJson}=    Set Variable        ${response.json()}
     Should Be Equal As Strings              ${responseJson['name']}   Small
 
index fcb4a53..a2241bc 100644 (file)
@@ -20,7 +20,7 @@ version: "3.7"
 services:
   #cps-standalone:
   #  container_name: cps-service
-  #  image: ps-service:${VERSION}
+  #  image: cps-service:${VERSION}
   #  volumes:
   #    - "./application.yml:/app/resources/application.yml"
   #  ports: