This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 778dd665b65 NIFI-16226 Parameter Context update fails when adding 
provider-backed inheritance in a cluster (#11562)
778dd665b65 is described below

commit 778dd665b6571c7baaa1c6930d4904b9df0f387f
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Aug 31 22:24:31 2026 +0200

    NIFI-16226 Parameter Context update fails when adding provider-backed 
inheritance in a cluster (#11562)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  76 ++++----
 .../nifi/web/StandardNiFiServiceFacadeTest.java    | 213 +++++++++++++++++++++
 .../nifi/web/api/ParameterContextResourceTest.java | 182 ++++++++++++++++++
 .../system/parameters/ParameterContextIT.java      |  59 ++++++
 4 files changed, 488 insertions(+), 42 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index 6688dd48152..abf4c573f16 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -1697,9 +1697,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         final List<ProcessGroup> groupsReferencingParameterContext = 
rootGroup.findAllProcessGroups(
                 group -> isGroupAffectedByParameterContext(group, 
parameterContextDto.getId()));
 
-        setEffectiveParameterUpdates(parameterContextDto);
-
-        final Set<String> updatedParameterNames = 
getUpdatedParameterNames(parameterContextDto);
+        final Set<String> updatedParameterNames = 
setEffectiveParameterUpdates(parameterContextDto);
 
         // Extend the updated parameter names with cascading names from 
parameter value references.
         // If a process group is bound to a context P whose local parameter X 
has the value #{Y}, and Y is
@@ -1785,12 +1783,13 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         return affectedComponentEntities;
     }
 
-    private void setEffectiveParameterUpdates(final ParameterContextDTO 
parameterContextDto) {
+    private Set<String> setEffectiveParameterUpdates(final ParameterContextDTO 
parameterContextDto) {
         final ParameterContext parameterContext = 
parameterContextDAO.getParameterContext(parameterContextDto.getId());
 
         final Map<String, Parameter> parameterUpdates = 
parameterContextDAO.getParameters(parameterContextDto, parameterContext);
         final List<ParameterContext> inheritedParameterContexts = 
parameterContextDAO.getInheritedParameterContexts(parameterContextDto);
         final Map<String, Parameter> proposedParameterUpdates = 
parameterContext.getEffectiveParameterUpdates(parameterUpdates, 
inheritedParameterContexts);
+        final Map<ParameterDescriptor, Parameter> localParameters = 
parameterContext.getParameters();
         final Map<String, ParameterEntity> parameterEntities = 
parameterContextDto.getParameters().stream()
                 .collect(Collectors.toMap(entity -> 
entity.getParameter().getName(), Function.identity()));
         parameterContextDto.getParameters().clear();
@@ -1798,6 +1797,12 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         for (final Entry<String, Parameter> entry : 
proposedParameterUpdates.entrySet()) {
             final String parameterName = entry.getKey();
             final Parameter parameter = entry.getValue();
+            final ParameterDescriptor parameterDescriptor = new 
ParameterDescriptor.Builder().name(parameterName).build();
+            final boolean locallyOwned = 
localParameters.containsKey(parameterDescriptor);
+            if (locallyOwned && !parameterEntities.containsKey(parameterName)) 
{
+                continue;
+            }
+
             final ParameterEntity parameterEntity;
             if (parameterEntities.containsKey(parameterName)) {
                 parameterEntity = parameterEntities.get(parameterName);
@@ -1810,12 +1815,31 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 parameterEntity = 
dtoFactory.createParameterEntity(parameterContext, parameter, revisionManager, 
parameterContextDAO);
             }
 
-            // Parameter is inherited if either this is the removal of a 
parameter not directly in this context, or it's parameter not specified 
directly in the DTO
-            final boolean isInherited = (parameter == null && 
!parameterContext.getParameters().containsKey(new 
ParameterDescriptor.Builder().name(parameterName).build()))
-                    || (parameter != null && 
!parameterEntities.containsKey(parameterName));
-            parameterEntity.getParameter().setInherited(isInherited);
+            if (parameter == null) {
+                parameterEntity.getParameter().setInherited(!locallyOwned);
+            } else {
+                final ParameterContext containingParameterContext = 
getContainingParameterContext(parameterContext, parameter, locallyOwned);
+                final boolean isInherited = !locallyOwned && 
!Objects.equals(parameterContext.getIdentifier(), 
containingParameterContext.getIdentifier());
+                final ParameterDTO parameterDto = 
parameterEntity.getParameter();
+                parameterDto.setProvided(parameter.isProvided());
+                parameterDto.setInherited(isInherited);
+                
parameterDto.setParameterContext(entityFactory.createParameterReferenceEntity(
+                        
dtoFactory.createParameterContextReference(containingParameterContext),
+                        
dtoFactory.createPermissionsDto(containingParameterContext)));
+            }
             parameterContextDto.getParameters().add(parameterEntity);
         }
+
+        return proposedParameterUpdates.keySet();
+    }
+
+    private ParameterContext getContainingParameterContext(final 
ParameterContext parameterContext, final Parameter parameter, final boolean 
locallyOwned) {
+        final String sourceContextId = parameter.getParameterContextId();
+        if (locallyOwned || sourceContextId == null || 
Objects.equals(parameterContext.getIdentifier(), sourceContextId)) {
+            return parameterContext;
+        }
+
+        return parameterContextDAO.getParameterContext(sourceContextId);
     }
 
     private void addReferencingComponents(final ControllerServiceNode service, 
final Set<ComponentNode> affectedComponents, final List<ParameterDTO> 
affectedParameterDtos,
@@ -1856,35 +1880,6 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         return false;
     }
 
-    private Set<String> getUpdatedParameterNames(final ParameterContextDTO 
parameterContextDto) {
-        final ParameterContext parameterContext = 
parameterContextDAO.getParameterContext(parameterContextDto.getId());
-
-        final Set<String> updatedParameters = new HashSet<>();
-        for (final ParameterEntity parameterEntity : 
parameterContextDto.getParameters()) {
-            final ParameterDTO parameterDto = parameterEntity.getParameter();
-            final String updatedValue = parameterDto.getValue();
-            final String parameterName = parameterDto.getName();
-
-            final Optional<Parameter> parameterOption = 
parameterContext.getParameter(parameterName);
-            if (!parameterOption.isPresent()) {
-                updatedParameters.add(parameterName);
-                continue;
-            }
-
-            final Parameter parameter = parameterOption.get();
-            final boolean valueUpdated = !Objects.equals(updatedValue, 
parameter.getValue());
-            // Sensitivity can be updated for provided parameters only
-            final boolean sensitivityUpdated = parameterDto.getSensitive() != 
null && parameterDto.getSensitive() != parameter.getDescriptor().isSensitive();
-            final boolean descriptionUpdated = parameterDto.getDescription() 
!= null && 
!parameterDto.getDescription().equals(parameter.getDescriptor().getDescription());
-            final boolean updated = valueUpdated || descriptionUpdated || 
sensitivityUpdated;
-            if (updated) {
-                updatedParameters.add(parameterName);
-            }
-        }
-
-        return updatedParameters;
-    }
-
     private Set<String> extendWithParameterValueReferences(final Set<String> 
updatedParameterNames, final List<ProcessGroup> 
groupsReferencingParameterContext) {
         final Set<String> extended = new HashSet<>(updatedParameterNames);
         for (final ProcessGroup group : groupsReferencingParameterContext) {
@@ -1897,10 +1892,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
                 if (referencedName == null || 
!updatedParameterNames.contains(referencedName)) {
                     continue;
                 }
-                final Optional<Parameter> referencedParam = 
groupContext.getParameter(referencedName);
-                if (referencedParam.isPresent()) {
-                    extended.add(entry.getKey().getName());
-                }
+                extended.add(entry.getKey().getName());
             }
         }
         return extended;
@@ -1928,7 +1920,7 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
         if (!contextParamName.equals(aliasTarget)) {
             return false;
         }
-        return groupContext.getParameter(contextParamName).isPresent();
+        return true;
     }
 
     @Override
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
index bc322addf40..dab62bdaecc 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/StandardNiFiServiceFacadeTest.java
@@ -59,6 +59,7 @@ import org.apache.nifi.controller.Counter;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.PropertyConfiguration;
+import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.controller.service.ControllerServiceNode;
 import org.apache.nifi.controller.service.ControllerServiceProvider;
@@ -86,6 +87,7 @@ import org.apache.nifi.parameter.Parameter;
 import org.apache.nifi.parameter.ParameterContext;
 import org.apache.nifi.parameter.ParameterContextLookup;
 import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.parameter.ParameterReferenceManager;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.registry.flow.FlowRegistryClientNode;
 import org.apache.nifi.registry.flow.FlowRegistryClientUserContext;
@@ -112,6 +114,7 @@ import 
org.apache.nifi.registry.flow.mapping.VersionedComponentFlowMapper;
 import org.apache.nifi.reporting.Bulletin;
 import org.apache.nifi.reporting.BulletinFactory;
 import org.apache.nifi.reporting.BulletinQuery;
+import org.apache.nifi.reporting.BulletinRepository;
 import org.apache.nifi.reporting.ComponentType;
 import org.apache.nifi.reporting.UserAwareEventAccess;
 import org.apache.nifi.services.FlowService;
@@ -130,6 +133,8 @@ import org.apache.nifi.web.api.dto.CountersSnapshotDTO;
 import org.apache.nifi.web.api.dto.DtoFactory;
 import org.apache.nifi.web.api.dto.EntityFactory;
 import org.apache.nifi.web.api.dto.ParameterContextDTO;
+import org.apache.nifi.web.api.dto.ParameterContextReferenceDTO;
+import org.apache.nifi.web.api.dto.ParameterDTO;
 import org.apache.nifi.web.api.dto.ProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RemoteProcessGroupDTO;
 import org.apache.nifi.web.api.dto.RevisionDTO;
@@ -148,6 +153,8 @@ import org.apache.nifi.web.api.entity.ConnectorEntity;
 import org.apache.nifi.web.api.entity.CopyRequestEntity;
 import org.apache.nifi.web.api.entity.CopyResponseEntity;
 import org.apache.nifi.web.api.entity.ParameterContextEntity;
+import org.apache.nifi.web.api.entity.ParameterContextReferenceEntity;
+import org.apache.nifi.web.api.entity.ParameterEntity;
 import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.SecretsEntity;
 import org.apache.nifi.web.api.entity.StatusHistoryEntity;
@@ -2429,6 +2436,212 @@ public class StandardNiFiServiceFacadeTest {
         verifyNoInteractions(dtoFactory);
     }
 
+    @Test
+    public void 
testGetComponentsAffectedByParameterContextUpdateTwicePreservesParameterProvenanceAndDetectsAliasChanges()
 {
+        final String targetContextId = "target-context";
+        final String inheritedContextId = "inherited-context";
+        final String inheritedParameterName = "inherited-provider-param";
+        final String inheritedParameterValue = "provider-value";
+        final String aliasParameterName = "alias-param";
+        final String aliasParameterValue = "#{" + inheritedParameterName + "}";
+        final String processorId = "processor-id";
+
+        final ParameterDescriptor inheritedDescriptor = new 
ParameterDescriptor.Builder().name(inheritedParameterName).build();
+        final Parameter inheritedParameter = new Parameter.Builder()
+                .descriptor(inheritedDescriptor)
+                .value(inheritedParameterValue)
+                .provided(true)
+                .parameterContextId(inheritedContextId)
+                .build();
+        final ParameterDescriptor aliasDescriptor = new 
ParameterDescriptor.Builder().name(aliasParameterName).build();
+        final Parameter aliasParameter = new Parameter.Builder()
+                .descriptor(aliasDescriptor)
+                .value(aliasParameterValue)
+                .parameterContextId(targetContextId)
+                .build();
+        final Parameter resolvedAliasParameter = new Parameter.Builder()
+                .fromParameter(aliasParameter)
+                .value(inheritedParameterValue)
+                .build();
+
+        final ParameterContext targetContext = mock(ParameterContext.class);
+        when(targetContext.getIdentifier()).thenReturn(targetContextId);
+        when(targetContext.getName()).thenReturn("Target Context");
+        when(targetContext.getParameters()).thenReturn(Map.of(aliasDescriptor, 
aliasParameter));
+        
when(targetContext.getParameterReferenceManager()).thenReturn(ParameterReferenceManager.EMPTY);
+
+        final ParameterContext inheritedContext = mock(ParameterContext.class);
+        when(inheritedContext.getIdentifier()).thenReturn(inheritedContextId);
+        when(inheritedContext.getName()).thenReturn("Inherited Context");
+        
when(inheritedContext.getInheritedParameterContexts()).thenReturn(List.of());
+
+        final ParameterContextDAO parameterContextDAO = 
mock(ParameterContextDAO.class);
+        
when(parameterContextDAO.getParameterContext(targetContextId)).thenReturn(targetContext);
+        
when(parameterContextDAO.getParameterContext(inheritedContextId)).thenReturn(inheritedContext);
+        when(parameterContextDAO.getParameters(any(ParameterContextDTO.class), 
same(targetContext))).thenReturn(Map.of());
+        
when(parameterContextDAO.getInheritedParameterContexts(any(ParameterContextDTO.class))).thenReturn(List.of(inheritedContext));
+        when(targetContext.getEffectiveParameterUpdates(anyMap(), 
eq(List.of(inheritedContext))))
+                .thenReturn(Map.of(inheritedParameterName, inheritedParameter, 
aliasParameterName, resolvedAliasParameter));
+
+        final ProcessorNode processorNode = mock(ProcessorNode.class);
+        when(processorNode.isRunning()).thenReturn(true);
+        
when(processorNode.getReferencedParameterNames()).thenReturn(Set.of(aliasParameterName));
+        when(processorNode.getIdentifier()).thenReturn(processorId);
+        when(processorNode.getName()).thenReturn("Processor");
+        when(processorNode.getProcessGroupIdentifier()).thenReturn("group-id");
+        
when(processorNode.getDesiredState()).thenReturn(ScheduledState.STOPPED);
+        when(processorNode.getActiveThreadCount()).thenReturn(0);
+        when(processorNode.getValidationErrors()).thenReturn(List.of());
+
+        final ProcessGroup referencingGroup = mock(ProcessGroup.class);
+        when(referencingGroup.getParameterContext()).thenReturn(targetContext);
+        
when(referencingGroup.getProcessors()).thenReturn(List.of(processorNode));
+        
when(referencingGroup.getControllerServices(false)).thenReturn(Set.of());
+        when(referencingGroup.getExecutionEngine()).thenReturn(null);
+        when(referencingGroup.getParent()).thenReturn(null);
+        when(referencingGroup.getIdentifier()).thenReturn("group-id");
+        when(referencingGroup.getName()).thenReturn("Group");
+        when(referencingGroup.isAuthorized(any(), any(), 
any())).thenReturn(false);
+        when(processorNode.getProcessGroup()).thenReturn(referencingGroup);
+
+        final ProcessGroup rootGroup = mock(ProcessGroup.class);
+        when(processGroupDAO.getProcessGroup("root")).thenReturn(rootGroup);
+        when(rootGroup.findAllProcessGroups(any())).thenAnswer(invocation -> {
+            final java.util.function.Predicate<ProcessGroup> predicate = 
invocation.getArgument(0);
+            return predicate.test(referencingGroup) ? 
List.of(referencingGroup) : List.of();
+        });
+
+        final ParameterContextReferenceDTO inheritedReference = new 
ParameterContextReferenceDTO();
+        inheritedReference.setId(inheritedContextId);
+        inheritedReference.setName("Inherited Context");
+
+        final ParameterContextReferenceEntity inheritedReferenceEntity = new 
ParameterContextReferenceEntity();
+        inheritedReferenceEntity.setId(inheritedContextId);
+        inheritedReferenceEntity.setComponent(inheritedReference);
+
+        final ParameterContextDTO parameterContextDto = new 
ParameterContextDTO();
+        parameterContextDto.setId(targetContextId);
+        parameterContextDto.setName("Target Context");
+        parameterContextDto.setParameters(new HashSet<>());
+        
parameterContextDto.setInheritedParameterContexts(List.of(inheritedReferenceEntity));
+
+        serviceFacade.setParameterContextDAO(parameterContextDAO);
+        serviceFacade.setRevisionManager(new NaiveRevisionManager());
+        final MockTestBulletinRepository bulletinRepository = new 
MockTestBulletinRepository();
+        serviceFacade.setBulletinRepository(bulletinRepository);
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setEntityFactory(new EntityFactory());
+        final BulletinRepository dtoBulletinRepository = 
mock(BulletinRepository.class);
+        when(dtoBulletinRepository.findBulletinsForSource(anyString(), 
anyString())).thenReturn(List.of());
+        dtoFactory.setBulletinRepository(dtoBulletinRepository);
+        serviceFacade.setDtoFactory(dtoFactory);
+
+        final Set<AffectedComponentEntity> firstAffected = 
serviceFacade.getComponentsAffectedByParameterContextUpdate(List.of(parameterContextDto));
+        assertEquals(1, firstAffected.size());
+        assertEquals(processorId, firstAffected.iterator().next().getId());
+
+        final Map<String, ParameterDTO> firstPassParameters = 
parameterContextDto.getParameters().stream()
+                .map(ParameterEntity::getParameter)
+                .collect(Collectors.toMap(ParameterDTO::getName, 
Function.identity()));
+        final ParameterDTO firstPassParameter = 
firstPassParameters.get(inheritedParameterName);
+        assertEquals(inheritedParameterName, firstPassParameter.getName());
+        assertTrue(firstPassParameter.getInherited());
+        assertTrue(firstPassParameter.getProvided());
+        assertEquals(inheritedContextId, 
firstPassParameter.getParameterContext().getId());
+        assertEquals(inheritedParameterValue, firstPassParameter.getValue());
+        assertEquals(1, firstPassParameter.getReferencingComponents().size());
+        assertFalse(firstPassParameters.containsKey(aliasParameterName));
+
+        final Set<AffectedComponentEntity> secondAffected = 
serviceFacade.getComponentsAffectedByParameterContextUpdate(List.of(parameterContextDto));
+        assertEquals(1, secondAffected.size());
+        assertEquals(processorId, secondAffected.iterator().next().getId());
+
+        final Map<String, ParameterDTO> secondPassParameters = 
parameterContextDto.getParameters().stream()
+                .map(ParameterEntity::getParameter)
+                .collect(Collectors.toMap(ParameterDTO::getName, 
Function.identity()));
+        final ParameterDTO secondPassParameter = 
secondPassParameters.get(inheritedParameterName);
+        assertEquals(inheritedParameterName, secondPassParameter.getName());
+        assertTrue(secondPassParameter.getInherited());
+        assertTrue(secondPassParameter.getProvided());
+        assertEquals(inheritedContextId, 
secondPassParameter.getParameterContext().getId());
+        assertEquals(inheritedParameterValue, secondPassParameter.getValue());
+        assertEquals(1, secondPassParameter.getReferencingComponents().size());
+        assertEquals(processorId, 
secondPassParameter.getReferencingComponents().iterator().next().getId());
+        assertFalse(secondPassParameters.containsKey(aliasParameterName));
+    }
+
+    @Test
+    public void 
testGetComponentsAffectedByParameterContextUpdateDoesNotAddSensitiveLocalAliasToUpdate()
 {
+        final String targetContextId = "target-context";
+        final String inheritedContextId = "inherited-context";
+        final String inheritedParameterName = "inherited-provider-param";
+        final String aliasParameterName = "alias-param";
+
+        final Parameter inheritedParameter = new Parameter.Builder()
+                .name(inheritedParameterName)
+                .value("provider-value")
+                .provided(true)
+                .parameterContextId(inheritedContextId)
+                .build();
+        final ParameterDescriptor aliasDescriptor = new 
ParameterDescriptor.Builder().name(aliasParameterName).sensitive(true).build();
+        final Parameter aliasParameter = new Parameter.Builder()
+                .descriptor(aliasDescriptor)
+                .value("#{" + inheritedParameterName + "}")
+                .parameterContextId(targetContextId)
+                .build();
+        final Parameter resolvedAliasParameter = new Parameter.Builder()
+                .fromParameter(aliasParameter)
+                .value("provider-value")
+                .build();
+
+        final ParameterContext targetContext = mock(ParameterContext.class);
+        when(targetContext.getIdentifier()).thenReturn(targetContextId);
+        when(targetContext.getParameters()).thenReturn(Map.of(aliasDescriptor, 
aliasParameter));
+        
when(targetContext.getParameterReferenceManager()).thenReturn(ParameterReferenceManager.EMPTY);
+
+        final ParameterContext inheritedContext = mock(ParameterContext.class);
+        when(inheritedContext.getIdentifier()).thenReturn(inheritedContextId);
+        when(inheritedContext.getName()).thenReturn("Inherited Context");
+
+        final ParameterContextDAO parameterContextDAO = 
mock(ParameterContextDAO.class);
+        
when(parameterContextDAO.getParameterContext(targetContextId)).thenReturn(targetContext);
+        
when(parameterContextDAO.getParameterContext(inheritedContextId)).thenReturn(inheritedContext);
+        when(parameterContextDAO.getParameters(any(ParameterContextDTO.class), 
same(targetContext))).thenReturn(Map.of());
+        
when(parameterContextDAO.getInheritedParameterContexts(any(ParameterContextDTO.class))).thenReturn(List.of(inheritedContext));
+        when(targetContext.getEffectiveParameterUpdates(anyMap(), 
eq(List.of(inheritedContext))))
+                .thenReturn(Map.of(inheritedParameterName, inheritedParameter, 
aliasParameterName, resolvedAliasParameter));
+
+        final ProcessGroup rootGroup = mock(ProcessGroup.class);
+        when(processGroupDAO.getProcessGroup("root")).thenReturn(rootGroup);
+        when(rootGroup.findAllProcessGroups(any())).thenReturn(List.of());
+
+        final ParameterContextReferenceDTO inheritedReference = new 
ParameterContextReferenceDTO();
+        inheritedReference.setId(inheritedContextId);
+        inheritedReference.setName("Inherited Context");
+        final ParameterContextReferenceEntity inheritedReferenceEntity = new 
ParameterContextReferenceEntity();
+        inheritedReferenceEntity.setId(inheritedContextId);
+        inheritedReferenceEntity.setComponent(inheritedReference);
+
+        final ParameterContextDTO parameterContextDto = new 
ParameterContextDTO();
+        parameterContextDto.setId(targetContextId);
+        parameterContextDto.setParameters(new HashSet<>());
+        
parameterContextDto.setInheritedParameterContexts(List.of(inheritedReferenceEntity));
+
+        serviceFacade.setParameterContextDAO(parameterContextDAO);
+        serviceFacade.setRevisionManager(new NaiveRevisionManager());
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setEntityFactory(new EntityFactory());
+        serviceFacade.setDtoFactory(dtoFactory);
+
+        
serviceFacade.getComponentsAffectedByParameterContextUpdate(List.of(parameterContextDto));
+
+        final Map<String, ParameterDTO> parameters = 
parameterContextDto.getParameters().stream()
+                .map(ParameterEntity::getParameter)
+                .collect(Collectors.toMap(ParameterDTO::getName, 
Function.identity()));
+        assertTrue(parameters.containsKey(inheritedParameterName));
+        assertFalse(parameters.containsKey(aliasParameterName));
+    }
+
     @Test
     public void 
testGetConnectorParameterContextThrowsWhenProcessGroupNotFound() {
         final String connectorId = "connector-id";
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/ParameterContextResourceTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/ParameterContextResourceTest.java
new file mode 100644
index 00000000000..2f89e986971
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/ParameterContextResourceTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+package org.apache.nifi.web.api;
+
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizeAccess;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.web.NiFiServiceFacade;
+import org.apache.nifi.web.api.dto.AffectedComponentDTO;
+import org.apache.nifi.web.api.dto.DtoFactory;
+import org.apache.nifi.web.api.dto.EntityFactory;
+import org.apache.nifi.web.api.dto.ParameterContextDTO;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+import org.apache.nifi.web.api.entity.AffectedComponentEntity;
+import org.apache.nifi.web.api.entity.ParameterContextEntity;
+import org.apache.nifi.web.api.entity.ParameterContextReferenceEntity;
+import org.apache.nifi.web.security.token.NiFiAuthenticationToken;
+import org.apache.nifi.web.util.ParameterUpdateManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ParameterContextResourceTest {
+
+    private static final String TARGET_CONTEXT_ID = "target-context";
+    private static final String CURRENT_INHERITED_CONTEXT_ID = 
"current-inherited-context";
+    private static final String REQUESTED_INHERITED_CONTEXT_ID = 
"requested-inherited-context";
+
+    @Mock
+    private NiFiServiceFacade serviceFacade;
+
+    @Mock
+    private Authorizer authorizer;
+
+    @Mock
+    private AuthorizableLookup lookup;
+
+    @Mock
+    private ParameterContext targetContext;
+
+    @Mock
+    private ParameterContext currentInheritedContext;
+
+    @Mock
+    private ParameterContext requestedInheritedContext;
+
+    @AfterEach
+    void tearDown() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    void 
testAuthorizeReadWriteParameterContextWithComponentsIncludesAffectedComponentsAndInheritedContexts()
 throws Exception {
+        final NiFiUser user = authenticate();
+
+        final ParameterContextResource resource = new 
ParameterContextResource();
+        resource.setServiceFacade(serviceFacade);
+        resource.setAuthorizer(authorizer);
+
+        final DtoFactory dtoFactory = new DtoFactory();
+        dtoFactory.setEntityFactory(new EntityFactory());
+
+        final ParameterUpdateManager parameterUpdateManager = spy(new 
ParameterUpdateManager(serviceFacade, dtoFactory, authorizer, resource));
+        setField(resource, "parameterUpdateManager", parameterUpdateManager);
+
+        final Set<AffectedComponentEntity> affectedComponents = new 
LinkedHashSet<>();
+        final AffectedComponentEntity processor = 
createAffectedComponent("processor-id", 
AffectedComponentDTO.COMPONENT_TYPE_PROCESSOR);
+        final AffectedComponentEntity controllerService = 
createAffectedComponent("controller-service-id", 
AffectedComponentDTO.COMPONENT_TYPE_CONTROLLER_SERVICE);
+        affectedComponents.add(processor);
+        affectedComponents.add(controllerService);
+
+        
doNothing().when(parameterUpdateManager).authorizeAffectedComponent(any(AffectedComponentEntity.class),
 same(lookup), same(user), eq(true), eq(true));
+
+        
when(targetContext.getInheritedParameterContexts()).thenReturn(List.of(currentInheritedContext));
+        
when(lookup.getParameterContext(TARGET_CONTEXT_ID)).thenReturn(targetContext);
+        
when(lookup.getParameterContext(REQUESTED_INHERITED_CONTEXT_ID)).thenReturn(requestedInheritedContext);
+
+        doAnswer(invocation -> {
+            final AuthorizeAccess authorizeAccess = invocation.getArgument(0);
+            authorizeAccess.authorize(lookup);
+            return null;
+        }).when(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+
+        final ParameterContextEntity requestEntity = new 
ParameterContextEntity();
+        requestEntity.setId(TARGET_CONTEXT_ID);
+        requestEntity.setRevision(new RevisionDTO());
+        requestEntity.setComponent(createRequestParameterContextDto());
+
+        final Method authorizeMethod = 
ParameterContextResource.class.getDeclaredMethod(
+                "authorizeReadWriteParameterContextWithComponents",
+                AuthorizableLookup.class,
+                String.class,
+                ParameterContextEntity.class,
+                Set.class,
+                NiFiUser.class
+        );
+        authorizeMethod.setAccessible(true);
+        authorizeMethod.invoke(resource, lookup, TARGET_CONTEXT_ID, 
requestEntity, affectedComponents, user);
+
+        verify(serviceFacade).authorizeAccess(any(AuthorizeAccess.class));
+        verify(targetContext).authorize(authorizer, RequestAction.READ, user);
+        verify(targetContext).authorize(authorizer, RequestAction.WRITE, user);
+        
verify(parameterUpdateManager).authorizeAffectedComponent(same(processor), 
same(lookup), same(user), eq(true), eq(true));
+        
verify(parameterUpdateManager).authorizeAffectedComponent(same(controllerService),
 same(lookup), same(user), eq(true), eq(true));
+        verify(currentInheritedContext).authorize(authorizer, 
RequestAction.READ, user);
+        verify(requestedInheritedContext).authorize(authorizer, 
RequestAction.READ, user);
+    }
+
+    private static ParameterContextDTO createRequestParameterContextDto() {
+        final ParameterContextReferenceEntity requestedInheritedReference = 
new ParameterContextReferenceEntity();
+        requestedInheritedReference.setId(REQUESTED_INHERITED_CONTEXT_ID);
+
+        final ParameterContextDTO dto = new ParameterContextDTO();
+        dto.setId(TARGET_CONTEXT_ID);
+        dto.setParameters(Set.of());
+        
dto.setInheritedParameterContexts(List.of(requestedInheritedReference));
+        return dto;
+    }
+
+    private static AffectedComponentEntity createAffectedComponent(final 
String componentId, final String referenceType) {
+        final AffectedComponentDTO dto = new AffectedComponentDTO();
+        dto.setId(componentId);
+        dto.setReferenceType(referenceType);
+
+        final AffectedComponentEntity entity = new AffectedComponentEntity();
+        entity.setId(componentId);
+        entity.setComponent(dto);
+        return entity;
+    }
+
+    private static NiFiUser authenticate() {
+        final NiFiUser user = new 
StandardNiFiUser.Builder().identity("unit-test-user").build();
+        final Authentication authentication = new NiFiAuthenticationToken(new 
NiFiUserDetails(user));
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        return user;
+    }
+
+    private static void setField(final Object target, final String fieldName, 
final Object value) throws Exception {
+        final Field field = target.getClass().getDeclaredField(fieldName);
+        field.setAccessible(true);
+        field.set(target, value);
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java
index 82105dbefec..65dcb282961 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java
@@ -196,6 +196,65 @@ public class ParameterContextIT extends NiFiSystemIT {
         waitForValidProcessor(processorId);
     }
 
+    @Test
+    @Timeout(30)
+    public void 
testExistingEmptyParameterContextCanInheritProviderBackedContext() throws 
NiFiClientException, IOException, InterruptedException {
+        if (getNumberOfNodes() > 1) {
+            waitForAllNodesConnected();
+        }
+
+        final String parameterName = "db.host";
+        final String parameterValue = "localhost";
+        final String parameterGroupName = "Parameters";
+        final String childContextName = getTestName() + " Child";
+        final String parentContextName = getTestName() + " Parent";
+
+        ParameterProviderEntity parameterProvider = 
createParameterProvider("PropertiesParameterProvider");
+        parameterProvider = 
updateParameterProviderProperties(parameterProvider,
+                Collections.singletonMap("parameters", parameterName + "=" + 
parameterValue));
+
+        final ParameterContextEntity childContextEntity = 
createParameterContextEntity(childContextName, null, Collections.emptySet(),
+                Collections.emptyList(), parameterProvider, 
parameterGroupName);
+        final ParameterContextEntity createdChildContext = 
getNifiClient().getParamContextClient().createParamContext(childContextEntity);
+
+        final ParameterGroupConfigurationEntity groupConfiguration = new 
ParameterGroupConfigurationEntity();
+        groupConfiguration.setSynchronized(true);
+        groupConfiguration.setGroupName(parameterGroupName);
+        groupConfiguration.setParameterContextName(childContextName);
+        
groupConfiguration.setParameterSensitivities(Collections.singletonMap(parameterName,
 ParameterSensitivity.NON_SENSITIVE));
+        fetchAndWaitForAppliedParameters(parameterProvider, 
Collections.singletonList(groupConfiguration));
+
+        final ParameterContextEntity createdParentContext = 
getNifiClient().getParamContextClient().createParamContext(
+                createParameterContextEntity(parentContextName, null, 
Collections.emptySet()));
+
+        final ParameterContextEntity parentUpdate = 
createParameterContextEntity(parentContextName, null, Collections.emptySet(),
+                Collections.singletonList(createdChildContext), null, null);
+        parentUpdate.setId(createdParentContext.getId());
+        parentUpdate.setRevision(createdParentContext.getRevision());
+        
parentUpdate.getComponent().setId(createdParentContext.getComponent().getId());
+
+        final ParameterContextUpdateRequestEntity updateRequest = 
getNifiClient().getParamContextClient().updateParamContext(parentUpdate);
+        final String requestId = updateRequest.getRequest().getRequestId();
+        
getClientUtil().waitForParameterContextRequestToComplete(createdParentContext.getId(),
 requestId);
+
+        final ParameterContextEntity persistedParent = 
getNifiClient().getParamContextClient().getParamContext(createdParentContext.getId(),
 false);
+        assertEquals(1, 
persistedParent.getComponent().getInheritedParameterContexts().size());
+        assertEquals(createdChildContext.getId(), 
persistedParent.getComponent().getInheritedParameterContexts().get(0).getId());
+
+        final ParameterDTO effectiveParameter = 
getNifiClient().getParamContextClient().getParamContext(createdParentContext.getId(),
 true)
+                .getComponent()
+                .getParameters()
+                .stream()
+                .map(ParameterEntity::getParameter)
+                .filter(parameter -> parameterName.equals(parameter.getName()))
+                .findFirst()
+                .orElse(null);
+        assertNotNull(effectiveParameter);
+        assertEquals(parameterValue, effectiveParameter.getValue());
+        assertSame(Boolean.TRUE, effectiveParameter.getInherited());
+        assertSame(Boolean.TRUE, effectiveParameter.getProvided());
+    }
+
     @Timeout(30)
     @Test
     public void testValidationWithRequiredPropertiesAndNoDefault() throws 
NiFiClientException, IOException, InterruptedException {

Reply via email to