From f05a5032d369a9cf9052bf82ed62bad5cc4ee68b Mon Sep 17 00:00:00 2001 From: Bruno Sakoto Date: Wed, 3 Mar 2021 18:27:54 -0500 Subject: Add basic authentication security 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 Change-Id: I3dfa0e49e5f4538c923e6bbe9bef976d30359fe6 --- cps-rest/pom.xml | 28 +++++++ .../org/onap/cps/config/WebSecurityConfig.java | 61 ++++++++++++++ .../rest/controller/AdminRestControllerSpec.groovy | 96 ++++++++++++++++------ .../rest/controller/DataRestControllerSpec.groovy | 66 ++++++++------- .../rest/controller/QueryRestControllerSpec.groovy | 18 ++-- .../controller/RestControllerSpecification.groovy | 34 ++++++++ .../exceptions/CpsRestExceptionHandlerSpec.groovy | 30 ++++++- csit/tests/cps-admin/cps-admin.robot | 16 ++-- csit/tests/cps-data/cps-data.robot | 6 +- docker-compose/docker-compose.yml | 2 +- 10 files changed, 284 insertions(+), 73 deletions(-) create mode 100644 cps-rest/src/main/java/org/onap/cps/config/WebSecurityConfig.java create mode 100644 cps-rest/src/test/groovy/org/onap/cps/rest/controller/RestControllerSpecification.groovy diff --git a/cps-rest/pom.xml b/cps-rest/pom.xml index 9264c6815..43875e8f4 100755 --- a/cps-rest/pom.xml +++ b/cps-rest/pom.xml @@ -1,3 +1,22 @@ + + @@ -34,6 +53,10 @@ org.springframework.boot spring-boot-starter-jetty + + org.springframework.boot + spring-boot-starter-security + io.swagger.core.v3 swagger-annotations @@ -86,6 +109,11 @@ + + org.springframework.security + spring-security-test + test + 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 index 000000000..943e02c27 --- /dev/null +++ b/cps-rest/src/main/java/org/onap/cps/config/WebSecurityConfig.java @@ -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); + } + +} diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy index ca99743ca..f38193803 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/AdminRestControllerSpec.groovy @@ -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() diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy index b9b680d35..ef834a7a2 100755 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/DataRestControllerSpec.groovy @@ -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' diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy index 0927c9d1e..907528a55 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/QueryRestControllerSpec.groovy @@ -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. @@ -19,14 +20,12 @@ 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 index 000000000..a700ea213 --- /dev/null +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/controller/RestControllerSpecification.groovy @@ -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 diff --git a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy index 4e10e2c14..5ddc9d95b 100644 --- a/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy +++ b/cps-rest/src/test/groovy/org/onap/cps/rest/exceptions/CpsRestExceptionHandlerSpec.groovy @@ -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, diff --git a/csit/tests/cps-admin/cps-admin.robot b/csit/tests/cps-admin/cps-admin.robot index 446a59b86..0e07f38aa 100644 --- a/csit/tests/cps-admin/cps-admin.robot +++ b/csit/tests/cps-admin/cps-admin.robot @@ -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 diff --git a/csit/tests/cps-data/cps-data.robot b/csit/tests/cps-data/cps-data.robot index 8b0202b3b..ff1e8d048 100644 --- a/csit/tests/cps-data/cps-data.robot +++ b/csit/tests/cps-data/cps-data.robot @@ -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 diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index fcb4a5381..a2241bcc2 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -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: -- cgit 1.2.3-korg