Policy Executor Feature Toggle

- defined config parameters for feature toggle and server details
- log request details when feature enabled
- improved JavaDoc in Controller
- improved configuration properties checks in HttpClientConfigurationSpec

Issue-ID: CPS-2311
Change-Id: I1ff4bd3592ce2570ac74f9ec6e62b75001cb611a
Signed-off-by: ToineSiebelink <toine.siebelink@est.tech>
diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
index bb2332d..a482cf5 100755
--- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
+++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyController.java
@@ -121,7 +121,7 @@
     /**
      * Query resource data from datastore.
      *
-     * @param datastoreName        name of the datastore
+     * @param datastoreName        name of the datastore (currently only supports "ncmp-datastore:operational")
      * @param cmHandle             cm handle identifier
      * @param cpsPath              CPS Path
      * @param optionsParamInQuery  options query parameter
@@ -144,9 +144,9 @@
     }
 
     /**
-     * Patch resource data from passthrough-running.
+     * Patch resource data.
      *
-     * @param datastoreName      name of the datastore
+     * @param datastoreName      name of the datastore (currently only supports "ncmp-datastore:passthrough-running")
      * @param cmHandle           cm handle identifier
      * @param resourceIdentifier resource identifier
      * @param requestBody        the request body
@@ -173,9 +173,9 @@
     }
 
     /**
-     * Create resource data in datastore pass-through running for given cm-handle.
+     * Create resource data for given cm-handle.
      *
-     * @param datastoreName      name of the datastore
+     * @param datastoreName      name of the datastore (currently only supports "ncmp-datastore:passthrough-running")
      * @param cmHandle           cm handle identifier
      * @param resourceIdentifier resource identifier
      * @param requestBody        the request body
@@ -198,9 +198,9 @@
     }
 
     /**
-     * Update resource data in datastore pass-through running for given cm-handle.
+     * Update resource data for given cm-handle.
      *
-     * @param datastoreName      name of the datastore
+     * @param datastoreName      name of the datastore (currently only supports "ncmp-datastore:passthrough-running")
      * @param cmHandle           cm handle identifier
      * @param resourceIdentifier resource identifier
      * @param requestBody        the request body
@@ -224,9 +224,9 @@
     }
 
     /**
-     * Delete resource data in datastore pass-through running for a given cm-handle.
+     * Delete resource data for a given cm-handle.
      *
-     * @param datastoreName      name of the datastore
+     * @param datastoreName      name of the datastore (currently only supports "ncmp-datastore:passthrough-running")
      * @param cmHandle           cm handle identifier
      * @param resourceIdentifier resource identifier
      * @param contentType        content type of the body
diff --git a/cps-ncmp-rest/src/test/resources/application.yml b/cps-ncmp-rest/src/test/resources/application.yml
index 9df1e58..aa57167 100644
--- a/cps-ncmp-rest/src/test/resources/application.yml
+++ b/cps-ncmp-rest/src/test/resources/application.yml
@@ -26,4 +26,4 @@
     enabled: true
     async:
         executor:
-            time-out-value-in-ms: 2000
\ No newline at end of file
+            time-out-value-in-ms: 2000
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java
index b902fe2..4cbf9d4 100644
--- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/DmiDataOperations.java
@@ -71,6 +71,7 @@
     private final JsonObjectMapper jsonObjectMapper;
     private final DmiProperties dmiProperties;
     private final DmiRestClient dmiRestClient;
+    private final PolicyExecutor policyExecutor;
 
     /**
      * This method fetches the resource data from the operational data store for a given CM handle
@@ -170,6 +171,9 @@
                                                                              final String dataType,
                                                                              final String authorization) {
         final YangModelCmHandle yangModelCmHandle = getYangModelCmHandle(cmHandleId);
+
+        policyExecutor.checkPermission(yangModelCmHandle, operationType, authorization, resourceId, requestData);
+
         final CmHandleState cmHandleState = yangModelCmHandle.getCompositeState().getCmHandleState();
         validateIfCmHandleStateReady(yangModelCmHandle, cmHandleState);
 
diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/PolicyExecutor.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/PolicyExecutor.java
new file mode 100644
index 0000000..2b5eb9e
--- /dev/null
+++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/data/PolicyExecutor.java
@@ -0,0 +1,74 @@
+/*
+ *  ============LICENSE_START=======================================================
+ *  Copyright (C) 2024 Nordix Foundation
+ *  ================================================================================
+ *  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.ncmp.impl.data;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.onap.cps.ncmp.api.data.models.OperationType;
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class PolicyExecutor {
+
+    @Value("${ncmp.policy-executor.enabled:false}")
+    private boolean enabled;
+
+    @Value("${ncmp.policy-executor.server.address:http://policy-executor}")
+    private String serverAddress;
+
+    @Value("${ncmp.policy-executor.server.port:8080}")
+    private String serverPort;
+
+    private static final String PAYLOAD_TYPE_PREFIX = "cm_";
+
+    /**
+     * Use the Policy Executor to check permission for a cm write operation.
+     * Wil throw an exception when the operation is not permitted (work in progress)
+     *
+     * @param yangModelCmHandle   the cm handle involved
+     * @param operationType       the write operation
+     * @param authorization       the original rest authorization token (can be used to determine the client)
+     * @param resourceIdentifier  the resource identifier (can be blank)
+     * @param changeRequestAsJson the change details from the original rest request in json format
+     */
+    public void checkPermission(final YangModelCmHandle yangModelCmHandle,
+                                final OperationType operationType,
+                                final String authorization,
+                                final String resourceIdentifier,
+                                final String changeRequestAsJson) {
+        if (enabled) {
+            final String payloadType = PAYLOAD_TYPE_PREFIX + operationType.getOperationName();
+            log.info("Policy Executor Enabled");
+            log.info("Address               : {}", serverAddress);
+            log.info("Port                  : {}", serverPort);
+            log.info("Authorization         : {}", authorization);
+            log.info("Payload Type          : {}", payloadType);
+            log.info("Target FDN            : {}", yangModelCmHandle.getAlternateId());
+            log.info("CM Handle Id          : {}", yangModelCmHandle.getId());
+            log.info("Resource Identifier   : {}", resourceIdentifier);
+            log.info("Change Request (json) : {}", changeRequestAsJson);
+        }
+    }
+}
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/config/HttpClientConfigurationSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/config/HttpClientConfigurationSpec.groovy
index 91aeb88..1d3b22f 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/config/HttpClientConfigurationSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/config/HttpClientConfigurationSpec.groovy
@@ -25,23 +25,21 @@
 import org.springframework.boot.context.properties.EnableConfigurationProperties
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.test.context.ContextConfiguration
-import org.springframework.test.context.TestPropertySource
 import spock.lang.Specification
 
 @SpringBootTest
 @ContextConfiguration(classes = [HttpClientConfiguration])
-@EnableConfigurationProperties(HttpClientConfiguration.class)
-@TestPropertySource(properties = ["ncmp.dmi.httpclient.data-services.readTimeoutInSeconds=789", "ncmp.dmi.httpclient.model-services.maximumConnectionsTotal=111"])
+@EnableConfigurationProperties(HttpClientConfiguration)
 class HttpClientConfigurationSpec extends Specification {
 
     @Autowired
-    private HttpClientConfiguration httpClientConfiguration
+    HttpClientConfiguration httpClientConfiguration
 
     def 'Test http client configuration properties of data with custom and default values'() {
         expect: 'properties are populated correctly for data'
             with(httpClientConfiguration.dataServices) {
                 assert connectionTimeoutInSeconds == 123
-                assert readTimeoutInSeconds == 789
+                assert readTimeoutInSeconds == 33
                 assert writeTimeoutInSeconds == 30
                 assert maximumConnectionsTotal == 100
                 assert pendingAcquireMaxCount == 22
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy
index c2bbfc2..970444f 100644
--- a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/DmiDataOperationsSpec.groovy
@@ -36,6 +36,7 @@
 import org.onap.cps.utils.JsonObjectMapper
 import org.spockframework.spring.SpringBean
 import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootContextLoader
 import org.springframework.boot.test.context.SpringBootTest
 import org.springframework.http.HttpStatus
 import org.springframework.http.ResponseEntity
@@ -53,7 +54,7 @@
 import static org.onap.cps.ncmp.utils.events.CloudEventMapper.toTargetEvent
 
 @SpringBootTest
-@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiProperties, DmiDataOperations])
+@ContextConfiguration(classes = [EventsPublisher, CpsApplicationContext, DmiProperties, DmiDataOperations, PolicyExecutor])
 class DmiDataOperationsSpec extends DmiOperationsBaseSpec {
 
     def NO_TOPIC = null
@@ -72,6 +73,9 @@
     @SpringBean
     EventsPublisher eventsPublisher = Stub()
 
+    @SpringBean
+    PolicyExecutor policyExecutor = Mock()
+
     def 'call get resource data for #expectedDataStore from DMI without topic #scenario.'() {
         given: 'a cm handle for #cmHandleId'
             mockYangModelCmHandleRetrieval(dmiProperties)
@@ -161,6 +165,8 @@
             def result = objectUnderTest.writeResourceDataPassThroughRunningFromDmi(cmHandleId, 'parent/child', operation, 'requestData', 'some data type', NO_AUTH_HEADER)
         then: 'the result is the response from the DMI service'
             assert result == responseFromDmi
+        and: 'the permission was checked with the policy executor'
+            1 * policyExecutor.checkPermission(_, operation, NO_AUTH_HEADER, resourceIdentifier, 'requestData' )
         where: 'the following operation is performed'
             operation || expectedOperationInUrl
             CREATE    || 'create'
diff --git a/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/PolicyExecutorSpec.groovy b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/PolicyExecutorSpec.groovy
new file mode 100644
index 0000000..6542067
--- /dev/null
+++ b/cps-ncmp-service/src/test/groovy/org/onap/cps/ncmp/impl/data/PolicyExecutorSpec.groovy
@@ -0,0 +1,73 @@
+package org.onap.cps.ncmp.impl.data
+
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
+import ch.qos.logback.classic.spi.ILoggingEvent
+import ch.qos.logback.core.read.ListAppender
+import org.onap.cps.ncmp.impl.inventory.models.YangModelCmHandle
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.ContextConfiguration
+import spock.lang.Specification
+
+import static org.onap.cps.ncmp.api.data.models.OperationType.PATCH
+
+@SpringBootTest
+@ContextConfiguration(classes = [PolicyExecutor])
+class PolicyExecutorSpec extends Specification {
+
+    @Autowired
+    PolicyExecutor objectUnderTest
+
+    def logAppender = Spy(ListAppender<ILoggingEvent>)
+
+    def setup() {
+        setupLogger()
+    }
+
+    def cleanup() {
+        ((Logger) LoggerFactory.getLogger(PolicyExecutor)).detachAndStopAllAppenders()
+    }
+
+    def 'Configuration properties.'() {
+        expect: 'properties used from application.yml'
+            assert objectUnderTest.enabled
+            assert objectUnderTest.serverAddress == 'http://localhost'
+            assert objectUnderTest.serverPort == '8785'
+    }
+
+    def 'Permission check logging.'() {
+        when: 'permission is checked for an operation'
+            def yangModelCmHandle = new YangModelCmHandle(id:'ch-1', alternateId:'fdn1')
+            objectUnderTest.checkPermission(yangModelCmHandle, PATCH, 'my credentials','my resource','my change')
+        then: 'correct details are logged '
+            assert getLogEntry(0) == 'Policy Executor Enabled'
+            assert getLogEntry(3).contains('my credentials')
+            assert getLogEntry(4).contains('cm_patch')
+            assert getLogEntry(5).contains('fdn1')
+            assert getLogEntry(6).contains('ch-1')
+            assert getLogEntry(7).contains('my resource')
+            assert getLogEntry(8).contains('my change')
+    }
+
+    def 'Permission check with feature disabled.'() {
+        given: 'feature is disabled'
+            objectUnderTest.enabled = false
+        when: 'permission is checked for an operation'
+            objectUnderTest.checkPermission(new YangModelCmHandle(), PATCH, 'my credentials','my resource','my change')
+        then: 'nothing is logged'
+            assert logAppender.list.isEmpty()
+    }
+
+    def setupLogger() {
+        def logger = LoggerFactory.getLogger(PolicyExecutor)
+        logger.setLevel(Level.DEBUG)
+        logger.addAppender(logAppender)
+        logAppender.start()
+    }
+
+    def getLogEntry(index) {
+        logAppender.list[index].formattedMessage
+    }
+}
diff --git a/cps-ncmp-service/src/test/resources/application.yml b/cps-ncmp-service/src/test/resources/application.yml
index 5b10e73..f0790dd 100644
--- a/cps-ncmp-service/src/test/resources/application.yml
+++ b/cps-ncmp-service/src/test/resources/application.yml
@@ -41,10 +41,12 @@
                 pendingAcquireMaxCount: 22
                 connectionTimeoutInSeconds: 123
                 maximumInMemorySizeInMegabytes: 7
+                readTimeoutInSeconds: 33
             model-services:
                 pendingAcquireMaxCount: 44
                 connectionTimeoutInSeconds: 456
                 maximumInMemorySizeInMegabytes: 8
+                maximumConnectionsTotal: 111
         auth:
             username: some-user
             password: some-password
@@ -59,6 +61,11 @@
         async-executor:
             parallelism-level: 3
 
+    policy-executor:
+        enabled: true
+        server:
+            address: "http://localhost"
+            port: "8785"
 
 # Custom Hazelcast Config.
 hazelcast: