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 {