46c0ddeb9389c36630aacff87d74fd19b0eb870c
[cps.git] /
1 /*
2  *  ============LICENSE_START=======================================================
3  *  Copyright (C) 2024 Nordix Foundation
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  *
17  *  SPDX-License-Identifier: Apache-2.0
18  *  ============LICENSE_END=========================================================
19  */
20
21 package org.onap.cps.ncmp.impl.data.policyexecutor
22
23 import ch.qos.logback.classic.Level
24 import ch.qos.logback.classic.Logger
25 import ch.qos.logback.classic.spi.ILoggingEvent
26 import ch.qos.logback.core.read.ListAppender
27 import com.fasterxml.jackson.databind.JsonNode
28 import com.fasterxml.jackson.databind.ObjectMapper
29 import org.onap.cps.ncmp.api.exceptions.NcmpException
30 import org.onap.cps.ncmp.api.exceptions.PolicyExecutorException
31 import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
32 import org.slf4j.LoggerFactory
33 import org.springframework.http.HttpStatus
34 import org.springframework.http.ResponseEntity
35 import org.springframework.web.reactive.function.client.WebClient
36 import reactor.core.publisher.Mono
37 import spock.lang.Specification
38
39 import java.util.concurrent.TimeoutException
40
41 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE
42 import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE
43 import static org.onap.cps.ncmp.api.data.models.OperationType.PATCH
44 import static org.onap.cps.ncmp.api.data.models.OperationType.UPDATE
45
46 class PolicyExecutorSpec extends Specification {
47
48     def mockWebClient = Mock(WebClient)
49     def mockRequestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
50     def mockResponseSpec = Mock(WebClient.ResponseSpec)
51     def spiedObjectMapper = Spy(ObjectMapper)
52
53     PolicyExecutor objectUnderTest = new PolicyExecutor(mockWebClient, spiedObjectMapper)
54
55     def logAppender = Spy(ListAppender<ILoggingEvent>)
56
57     def someValidJson = '{"Hello":"World"}'
58
59     def setup() {
60         setupLogger()
61         objectUnderTest.enabled = true
62         mockWebClient.post() >> mockRequestBodyUriSpec
63         mockRequestBodyUriSpec.uri(*_) >> mockRequestBodyUriSpec
64         mockRequestBodyUriSpec.header(*_) >> mockRequestBodyUriSpec
65         mockRequestBodyUriSpec.body(*_) >> mockRequestBodyUriSpec
66         mockRequestBodyUriSpec.retrieve() >> mockResponseSpec
67     }
68
69     def cleanup() {
70         ((Logger) LoggerFactory.getLogger(PolicyExecutor)).detachAndStopAllAppenders()
71     }
72
73     def 'Permission check with "allow" decision.'() {
74         given: 'allow response'
75             mockResponse([decision:'allow'], HttpStatus.OK)
76         when: 'permission is checked for an operation'
77             objectUnderTest.checkPermission(new YangModelCmHandle(), operationType, 'my credentials','my resource',someValidJson)
78         then: 'system logs the operation is allowed'
79             assert getLogEntry(2) == 'Operation allowed.'
80         and: 'no exception occurs'
81             noExceptionThrown()
82         where: 'all write operations are tested'
83             operationType << [ CREATE, DELETE, PATCH, UPDATE ]
84     }
85
86     def 'Permission check with "other" decision (not allowed).'() {
87         given: 'other response'
88             mockResponse([decision:'other', decisionId:123, message:'I dont like Mondays' ], HttpStatus.OK)
89         when: 'permission is checked for an operation'
90             objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
91         then: 'Policy Executor exception is thrown'
92             def thrownException = thrown(PolicyExecutorException)
93             assert thrownException.message == 'Operation not allowed. Decision id 123 : other'
94             assert thrownException.details == 'I dont like Mondays'
95     }
96
97     def 'Permission check with non-2xx response and "allow" default decision.'() {
98         given: 'other http response'
99             mockResponse([], HttpStatus.I_AM_A_TEAPOT)
100         and: 'the configured default decision is "allow"'
101             objectUnderTest.defaultDecision = 'allow'
102         when: 'permission is checked for an operation'
103             objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
104         then: 'No exception is thrown'
105             noExceptionThrown()
106     }
107
108     def 'Permission check with non-2xx response and "other" default decision.'() {
109         given: 'other http response'
110             mockResponse([], HttpStatus.I_AM_A_TEAPOT)
111         and: 'the configured default decision is NOT "allow"'
112             objectUnderTest.defaultDecision = 'deny by default'
113         when: 'permission is checked for an operation'
114             objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials', 'my resource', someValidJson)
115         then: 'Policy Executor exception is thrown'
116             def thrownException = thrown(PolicyExecutorException)
117             assert thrownException.message == 'Operation not allowed. Decision id N/A : deny by default'
118             assert thrownException.details == 'Policy Executor returned HTTP Status code 418. Falling back to configured default decision: deny by default'
119     }
120
121     def 'Permission check with invalid response from Policy Executor.'() {
122         given: 'invalid response from Policy executor'
123             mockResponseSpec.toEntity(*_) >> invalidResponse
124         when: 'permission is checked for an operation'
125             objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
126         then: 'system logs the expected message'
127             assert getLogEntry(1) == expectedMessage
128         where: 'following invalid responses are received'
129             invalidResponse                                      || expectedMessage
130             Mono.empty()                                         || 'No valid response from Policy Executor, ignored'
131             Mono.just(new ResponseEntity<>(null, HttpStatus.OK)) || 'No valid response body from Policy Executor, ignored'
132     }
133
134     def 'Permission check with timeout exception.'() {
135         given: 'a timeout during the request'
136             def cause = new TimeoutException()
137             mockResponseSpec.toEntity(*_) >> { throw new RuntimeException(cause) }
138         and: 'the configured default decision is NOT "allow"'
139             objectUnderTest.defaultDecision = 'deny by default'
140         when: 'permission is checked for an operation'
141             objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
142         then: 'Policy Executor exception is thrown'
143             def thrownException = thrown(PolicyExecutorException)
144             assert thrownException.message == 'Operation not allowed. Decision id N/A : deny by default'
145             assert thrownException.details == 'Policy Executor request timed out. Falling back to configured default decision: deny by default'
146     }
147
148     def 'Permission check with other runtime exception.'() {
149         given: 'some other runtime exception'
150             def originalException =  new RuntimeException()
151             mockResponseSpec.toEntity(*_) >> { throw originalException}
152         when: 'permission is checked for an operation'
153             objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', someValidJson)
154         then: 'The original exception is thrown'
155             def thrownException = thrown(RuntimeException)
156             assert thrownException == originalException
157     }
158
159     def 'Permission check with an invalid change request json.'() {
160         when: 'permission is checked for an invalid change request'
161             objectUnderTest.checkPermission(new YangModelCmHandle(), CREATE, 'my credentials', 'my resource', 'invalid json string')
162         then: 'an ncmp exception thrown'
163             def ncmpException = thrown(NcmpException)
164             ncmpException.message == 'Cannot convert Change Request data to Object'
165             ncmpException.details.contains('invalid json string')
166     }
167
168     def 'Permission check feature disabled.'() {
169         given: 'feature is disabled'
170             objectUnderTest.enabled = false
171         when: 'permission is checked for an operation'
172             objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource',someValidJson)
173         then: 'system logs that the feature not enabled'
174             assert getLogEntry(0) == 'Policy Executor Enabled: false'
175     }
176
177     def mockResponse(mockResponseAsMap, httpStatus) {
178         JsonNode jsonNode = spiedObjectMapper.readTree(spiedObjectMapper.writeValueAsString(mockResponseAsMap))
179         def mono = Mono.just(new ResponseEntity<>(jsonNode, httpStatus))
180         mockResponseSpec.toEntity(*_) >> mono
181     }
182
183     def setupLogger() {
184         def logger = LoggerFactory.getLogger(PolicyExecutor)
185         logger.setLevel(Level.TRACE)
186         logger.addAppender(logAppender)
187         logAppender.start()
188     }
189
190     def getLogEntry(index) {
191         logAppender.list[index].formattedMessage
192     }
193 }