Handle Root MO modify scenario in ProvMnS 84/143084/1
authorseanbeirne <sean.beirne@est.tech>
Thu, 29 Jan 2026 12:40:37 +0000 (12:40 +0000)
committerseanbeirne <sean.beirne@est.tech>
Thu, 29 Jan 2026 13:08:35 +0000 (13:08 +0000)
- Authentication fix for PUT request

Issue-ID: CPS-3151
Change-Id: Ic235ff01488ba417da1378970c319c3650714bd5
Signed-off-by: seanbeirne <sean.beirne@est.tech>
cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/ProvMnSController.java
cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/ProvMnSControllerSpec.groovy
cps-ncmp-service/src/main/java/org/onap/cps/ncmp/impl/dmi/DmiRestClient.java

index b23436c..2662e94 100644 (file)
@@ -21,6 +21,7 @@
 package org.onap.cps.ncmp.rest.controller;
 
 import static org.onap.cps.ncmp.api.data.models.OperationType.CREATE;
+import static org.onap.cps.ncmp.api.data.models.OperationType.DELETE;
 import static org.onap.cps.ncmp.impl.models.RequiredDmiService.DATA;
 import static org.onap.cps.ncmp.impl.provmns.ParameterHelper.NO_OP;
 import static org.springframework.http.HttpStatus.BAD_REQUEST;
@@ -191,21 +192,23 @@ public class ProvMnSController implements ProvMnS {
         final Map<String, List<ClassInstance>> changeRequestAsMap = new HashMap<>(1);
         changeRequestAsMap.put(operationDetails.className(), operationDetails.ClassInstances());
         final String changeRequestAsJson = jsonObjectMapper.asJsonString(changeRequestAsMap);
-        final String resourceIdentifier;
-        if (operationDetails.parentFdn().length() <= yangModelCmHandle.getAlternateId().length()) {
-            if (operationDetails.parentFdn().isEmpty()) {
-                resourceIdentifier = "/";
-            } else {
-                resourceIdentifier = operationDetails.parentFdn();
-            }
-        } else {
-            final int index = yangModelCmHandle.getAlternateId().length();
-            resourceIdentifier = operationDetails.parentFdn().substring(index);
+        if (targetIsRootMo(yangModelCmHandle.getAlternateId(), operationDetails)) {
+            throw new DataValidationException("Data manipulation operations are not supported on "
+                + requestParameters.fdn(), "");
         }
+        final int index = yangModelCmHandle.getAlternateId().length();
+        final String resourceIdentifier = operationDetails.parentFdn().substring(index);
         policyExecutor.checkPermission(yangModelCmHandle, operationDetails.operationType(),
             requestParameters.authorization(), resourceIdentifier, changeRequestAsJson);
     }
 
+    private static boolean targetIsRootMo(final String alternateId, final OperationDetails operationDetails) {
+        if (DELETE.equals(operationDetails.operationType())) {
+            return operationDetails.parentFdn().length() <= alternateId.length();
+        }
+        return operationDetails.parentFdn().length() < alternateId.length();
+    }
+
     private void checkPermissionForEachPatchItem(final String baseFdn,
                                                  final List<PatchItem> patchItems,
                                                  final YangModelCmHandle yangModelCmHandle,
index ba2bd5e..e014017 100644 (file)
@@ -253,9 +253,35 @@ class ProvMnSControllerSpec extends Specification {
             'modify grandchild'      | patchMediaType      | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2/child=id1' | patchJsonBody      || '/child=id1'                        | '{"otherChild":[{"id":"id2","attributes":{"attr1":"test"}}]}'
             '3gpp modify grandchild' | patchMediaType3gpp  | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2/child=id1' | patchJsonBody3gpp  || '/child=id1'                        | '{"otherChild":[{"id":"id2","attributes":{"attr1":"test"}}]}'
             'no subnetwork'          | patchMediaType      | '/managedElement=2'              | '/managedElement=2/child=id1'              | patchJsonBody      || '/child=id1'                        | '{"otherChild":[{"id":"id2","attributes":{"attr1":"test"}}]}'
-            'modify first child'     | patchMediaType      | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2'           | patchJsonBody      || '/subnetwork=1/managedElement=2'    | '{"otherChild":[{"id":"id2","attributes":{"attr1":"test"}}]}'
-            'modify alternate id'    | patchMediaType      | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2'           | patchWithoutChild  || '/subnetwork=1'                     | '{"managedElement":[{"id":"2","attributes":{"attr2":"test2"}}]}'
-            'modify root MO'         | patchMediaType      | '/managedElement=2'              | '/managedElement=2'                        | patchWithoutChild  || '/'                                 | '{"managedElement":[{"id":"2","attributes":{"attr2":"test2"}}]}'
+            'modify first child'     | patchMediaType      | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2'           | patchJsonBody      || ''                                  | '{"otherChild":[{"id":"id2","attributes":{"attr1":"test"}}]}'
+    }
+
+    def 'Attempt Patch request with root MO, #scenario.'() {
+        given: 'provmns url'
+            def provmnsUrl = "$provMnSBasePath/v1$fdn"
+        and: 'alternate Id can be matched'
+            mockedCmHandle.getAlternateId() >> alternateId
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(fdn, "/") >> 'mock'
+        and: 'persistence service returns yangModelCmHandle'
+            mockInventoryPersistence.getYangModelCmHandle('mock') >> mockedCmHandle
+        when: 'patch request is performed'
+            def response = mvc.perform(patch(provmnsUrl)
+                    .header('Authorization', 'my authorization')
+                    .contentType(patchMediaType)
+                    .content(jsonBody))
+                    .andReturn().response
+        then: 'policy executor is never invoked'
+            0 * mockPolicyExecutor._
+        and: 'the response status is BAD_REQUEST'
+            assert response.status == BAD_REQUEST.value()
+        and: 'the response content contains the required error details'
+            assert response.contentAsString.contains('VALIDATION_ERROR')
+            assert response.contentAsString.contains('not supported')
+            assert response.contentAsString.contains(fdn)
+        where: 'following scenarios are applied'
+            scenario              | alternateId                      | fdn                              | jsonBody
+            'modify alternate id' | '/subnetwork=1/managedElement=2' | '/subnetwork=1/managedElement=2' | patchWithoutChild
+            'modify root MO'      | '/managedElement=2'              | '/managedElement=2'              | patchWithoutChild
     }
 
     def 'Patch request with error from DMI.'() {
@@ -487,4 +513,28 @@ class ProvMnSControllerSpec extends Specification {
             assert response.contentAsString.contains('"title":"/myClass=id1 not found"')
     }
 
+    def 'Attempt Delete root MO, #scenario.'() {
+        given: 'resource data url'
+            def deleteUrl = "$provMnSBasePath/v1$fdn"
+        and: 'alternate Id is mocked can be matched'
+            mockedCmHandle.getAlternateId() >> alternateId
+            mockAlternateIdMatcher.getCmHandleIdByLongestMatchingAlternateId(fdn, "/") >> 'mock'
+        and: 'persistence service returns yangModelCmHandle'
+            mockInventoryPersistence.getYangModelCmHandle('mock') >> mockedCmHandle
+        when: 'Delete data resource request is attempted'
+            def response = mvc.perform(delete(deleteUrl).header('Authorization', 'my authorization')).andReturn().response
+        then: 'policy executor is never invoked'
+            0 * mockPolicyExecutor._
+        and: 'the response status is BAD_REQUEST'
+            assert response.status == BAD_REQUEST.value()
+        and: 'the response content contains the required error details'
+            assert response.contentAsString.contains('VALIDATION_ERROR')
+            assert response.contentAsString.contains('not supported')
+            assert response.contentAsString.contains(fdn)
+        where: 'root MOs are targeted'
+            scenario          | fdn                              | alternateId
+            'with subnetwork' | '/Subnetwork=1/ManagedElement=1' | '/subnetwork=1/managedElement=1'
+            'no subnetwork'   | '/ManagedElement=1'              | '/managedElement=1'
+    }
+
 }
index 752ce85..d1172ce 100644 (file)
@@ -154,7 +154,7 @@ public class DmiRestClient {
         return getWebClient(requiredDmiService)
             .put()
             .uri(urlTemplateParameters.urlTemplate(), urlTemplateParameters.urlVariables())
-            .headers(httpHeaders -> configureHttpHeaders(httpHeaders, NO_AUTHORIZATION))
+            .headers(httpHeaders -> configureHttpHeaders(httpHeaders, authorization))
             .bodyValue(body)
             .exchangeToMono(this::createIdenticalResponseForClient)
             .block();