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 42df8ed0e5 NIFI-15561 Ignored Sensitive Status for Parameter
References in Ghosted Components (#10865)
42df8ed0e5 is described below
commit 42df8ed0e53e1c25fd852c192ddd4f9b3f34e61f
Author: Mark Payne <[email protected]>
AuthorDate: Sat Feb 7 14:04:05 2026 -0500
NIFI-15561 Ignored Sensitive Status for Parameter References in Ghosted
Components (#10865)
Signed-off-by: David Handermann <[email protected]>
---
.../nifi/parameter/StandardParameterContext.java | 8 ++
.../parameter/TestStandardParameterContext.java | 93 ++++++++++--
...ParameterSensitivityWithGhostedComponentIT.java | 158 +++++++++++++++++++++
3 files changed, 251 insertions(+), 8 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
index 0489e618ec..4a70e32d26 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
@@ -776,6 +776,10 @@ public class StandardParameterContext implements
ParameterContext {
final boolean isDeletion = (parameter == null);
final String action = isDeletion ? "remove" : "update";
for (final ProcessorNode procNode :
parameterReferenceManager.getProcessorsReferencing(this, parameterName)) {
+ if (procNode.isExtensionMissing()) {
+ continue;
+ }
+
if (procNode.isRunning() && (isDeletion || duringUpdate)) {
throw new IllegalStateException("Cannot " + action + "
parameter '" + parameterName + "' because it is referenced by " + procNode + ",
which is currently running");
}
@@ -786,6 +790,10 @@ public class StandardParameterContext implements
ParameterContext {
}
for (final ControllerServiceNode serviceNode :
parameterReferenceManager.getControllerServicesReferencing(this,
parameterName)) {
+ if (serviceNode.isExtensionMissing()) {
+ continue;
+ }
+
final ControllerServiceState serviceState = serviceNode.getState();
if (serviceState != ControllerServiceState.DISABLED && (isDeletion
|| duringUpdate)) {
throw new IllegalStateException("Cannot " + action + "
parameter '" + parameterName + "' because it is referenced by "
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
index efc6528318..31a6793018 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
@@ -16,7 +16,9 @@
*/
package org.apache.nifi.parameter;
+import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.PropertyConfiguration;
import org.apache.nifi.controller.service.ControllerServiceNode;
import org.apache.nifi.controller.service.ControllerServiceState;
import org.apache.nifi.groups.ProcessGroup;
@@ -40,6 +42,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
public class TestStandardParameterContext {
@@ -370,8 +374,8 @@ public class TestStandardParameterContext {
}
private static ProcessorNode getProcessorNode(String parameterName,
HashMapParameterReferenceManager referenceManager) {
- final ProcessorNode procNode = Mockito.mock(ProcessorNode.class);
- Mockito.when(procNode.isRunning()).thenReturn(false);
+ final ProcessorNode procNode = mock(ProcessorNode.class);
+ when(procNode.isRunning()).thenReturn(false);
referenceManager.addProcessorReference(parameterName, procNode);
return procNode;
}
@@ -385,17 +389,90 @@ public class TestStandardParameterContext {
}
private static void setProcessorRunning(final ProcessorNode processorNode,
final boolean isRunning) {
- Mockito.when(processorNode.isRunning()).thenReturn(isRunning);
+ when(processorNode.isRunning()).thenReturn(isRunning);
}
private static void setControllerServiceState(final ControllerServiceNode
serviceNode, final ControllerServiceState state) {
- Mockito.when(serviceNode.getState()).thenReturn(state);
+ when(serviceNode.getState()).thenReturn(state);
}
private static void enableControllerService(final ControllerServiceNode
serviceNode) {
setControllerServiceState(serviceNode, ControllerServiceState.ENABLED);
}
+ @Test
+ public void testGhostedProcessorSkippedDuringParameterValidation() {
+ final HashMapParameterReferenceManager referenceManager = new
HashMapParameterReferenceManager();
+ final ParameterContext context =
createStandardParameterContext(referenceManager);
+
+ final ProcessorNode procNode = getProcessorNode("abc",
referenceManager);
+ when(procNode.isExtensionMissing()).thenReturn(true);
+
+ // Set up the ghosted processor to reference "abc" via a sensitive
property
+ final PropertyDescriptor sensitiveProperty = new
PropertyDescriptor.Builder().name("sensitive-prop").sensitive(true).build();
+ final ParameterReference paramReference =
mock(ParameterReference.class);
+ when(paramReference.getParameterName()).thenReturn("abc");
+ final PropertyConfiguration propertyConfig =
mock(PropertyConfiguration.class);
+
when(propertyConfig.getParameterReferences()).thenReturn(Collections.singletonList(paramReference));
+
when(procNode.getProperties()).thenReturn(Collections.singletonMap(sensitiveProperty,
propertyConfig));
+
+ // Adding parameter "abc" as non-sensitive should succeed despite the
sensitivity mismatch because the processor is ghosted
+ final ParameterDescriptor abcDescriptor = new
ParameterDescriptor.Builder().name("abc").sensitive(false).build();
+ final Map<String, Parameter> parameters = new HashMap<>();
+ parameters.put("abc", createParameter(abcDescriptor, "123"));
+ context.setParameters(parameters);
+ assertEquals("123", context.getParameter("abc").get().getValue());
+
+ // Updating the parameter value should succeed because the processor
is ghosted
+ parameters.clear();
+ parameters.put("abc", createParameter(abcDescriptor, "321"));
+ context.setParameters(parameters);
+ assertEquals("321", context.getParameter("abc").get().getValue());
+
+ // Deleting the parameter should succeed because the processor is
ghosted
+ parameters.clear();
+ parameters.put("abc", null);
+ context.setParameters(parameters);
+ assertFalse(context.getParameter("abc").isPresent());
+ }
+
+ @Test
+ public void testGhostedControllerServiceSkippedDuringParameterValidation()
{
+ final HashMapParameterReferenceManager referenceManager = new
HashMapParameterReferenceManager();
+ final ParameterContext context =
createStandardParameterContext(referenceManager);
+
+ final ControllerServiceNode serviceNode =
mock(ControllerServiceNode.class);
+ when(serviceNode.isExtensionMissing()).thenReturn(true);
+ referenceManager.addControllerServiceReference("abc", serviceNode);
+
+ // Set up the ghosted controller service to reference "abc" via a
non-sensitive property
+ final PropertyDescriptor nonSensitiveProperty = new
PropertyDescriptor.Builder().name("non-sensitive-prop").sensitive(false).build();
+ final ParameterReference paramReference =
mock(ParameterReference.class);
+ when(paramReference.getParameterName()).thenReturn("abc");
+ final PropertyConfiguration propertyConfig =
mock(PropertyConfiguration.class);
+
when(propertyConfig.getParameterReferences()).thenReturn(Collections.singletonList(paramReference));
+
when(serviceNode.getProperties()).thenReturn(Collections.singletonMap(nonSensitiveProperty,
propertyConfig));
+
+ // Adding parameter "abc" as sensitive should succeed despite the
sensitivity mismatch because the controller service is ghosted
+ final ParameterDescriptor abcDescriptor = new
ParameterDescriptor.Builder().name("abc").sensitive(true).build();
+ final Map<String, Parameter> parameters = new HashMap<>();
+ parameters.put("abc", createParameter(abcDescriptor, "123"));
+ context.setParameters(parameters);
+ assertEquals("123", context.getParameter("abc").get().getValue());
+
+ // Updating the parameter value should succeed because the controller
service is ghosted
+ parameters.clear();
+ parameters.put("abc", createParameter(abcDescriptor, "321"));
+ context.setParameters(parameters);
+ assertEquals("321", context.getParameter("abc").get().getValue());
+
+ // Deleting the parameter should succeed because the controller
service is ghosted
+ parameters.clear();
+ parameters.put("abc", null);
+ context.setParameters(parameters);
+ assertFalse(context.getParameter("abc").isPresent());
+ }
+
@Test
public void testAlertReferencingComponents() {
final String inheritedParamName = "def";
@@ -403,9 +480,9 @@ public class TestStandardParameterContext {
final HashMapParameterReferenceManager referenceManager =
Mockito.spy(new HashMapParameterReferenceManager());
final Set<ProcessGroup> processGroups = new HashSet<>();
- final ProcessGroup processGroup = Mockito.mock(ProcessGroup.class);
+ final ProcessGroup processGroup = mock(ProcessGroup.class);
processGroups.add(processGroup);
-
Mockito.when(referenceManager.getProcessGroupsBound(ArgumentMatchers.any())).thenReturn(processGroups);
+
when(referenceManager.getProcessGroupsBound(ArgumentMatchers.any())).thenReturn(processGroups);
final StandardParameterContextManager parameterContextLookup = new
StandardParameterContextManager();
final ParameterContext a = createParameterContext("a",
parameterContextLookup, referenceManager);
addParameter(a, "abc", "123");
@@ -446,7 +523,7 @@ public class TestStandardParameterContext {
// Param abc
// (Inherited) Param def (from B)
- final ControllerServiceNode serviceNode =
Mockito.mock(ControllerServiceNode.class);
+ final ControllerServiceNode serviceNode =
mock(ControllerServiceNode.class);
enableControllerService(serviceNode);
referenceManager.addControllerServiceReference(inheritedParamName,
serviceNode);
@@ -476,7 +553,7 @@ public class TestStandardParameterContext {
public void testChangingParameterForEnabledControllerService() {
final HashMapParameterReferenceManager referenceManager = new
HashMapParameterReferenceManager();
final ParameterContext context =
createStandardParameterContext(referenceManager);
- final ControllerServiceNode serviceNode =
Mockito.mock(ControllerServiceNode.class);
+ final ControllerServiceNode serviceNode =
mock(ControllerServiceNode.class);
enableControllerService(serviceNode);
final ParameterDescriptor abcDescriptor = new
ParameterDescriptor.Builder().name("abc").sensitive(true).build();
diff --git
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterSensitivityWithGhostedComponentIT.java
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterSensitivityWithGhostedComponentIT.java
new file mode 100644
index 0000000000..fd591c06eb
--- /dev/null
+++
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterSensitivityWithGhostedComponentIT.java
@@ -0,0 +1,158 @@
+/*
+ * 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.tests.system.parameters;
+
+import org.apache.nifi.tests.system.NiFiInstance;
+import org.apache.nifi.tests.system.NiFiSystemIT;
+import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.entity.ParameterContextEntity;
+import org.apache.nifi.web.api.entity.ParameterContextUpdateRequestEntity;
+import org.apache.nifi.web.api.entity.ProcessorEntity;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * System test that verifies parameter updates succeed when a referencing
processor is ghosted.
+ *
+ * When a processor is ghosted (its NAR is missing), all of its properties are
treated as sensitive because
+ * NiFi does not know the actual property descriptors. This means a
non-sensitive parameter that was originally
+ * referenced from a non-sensitive property now appears to be referenced from
a sensitive property, which would
+ * normally block any update to that parameter. The fix in
StandardParameterContext skips validation for ghosted
+ * components entirely, allowing the parameter to be updated.
+ *
+ * This test exercises the full lifecycle:
+ * 1. A processor references a non-sensitive parameter via a non-sensitive
property.
+ * 2. The processor's NAR is removed and NiFi is restarted, causing the
processor to become ghosted.
+ * 3. While the processor is ghosted, the parameter value is updated
(remaining non-sensitive).
+ * 4. The NAR is restored and NiFi is restarted.
+ * 5. The processor is no longer ghosted and can still be used with the
updated parameter value.
+ */
+public class ParameterSensitivityWithGhostedComponentIT extends NiFiSystemIT {
+ private static final Logger logger =
LoggerFactory.getLogger(ParameterSensitivityWithGhostedComponentIT.class);
+
+ private static final String TEST_EXTENSIONS_NAR_PREFIX =
"nifi-system-test-extensions-nar";
+
+ @Override
+ protected boolean isDestroyEnvironmentAfterEachTest() {
+ return true;
+ }
+
+ @Override
+ protected boolean isAllowFactoryReuse() {
+ return false;
+ }
+
+ @Test
+ public void testParameterUpdateWhileProcessorGhosted() throws
NiFiClientException, IOException, InterruptedException {
+ // Create a parameter context with a non-sensitive parameter
+ final ParameterContextEntity paramContext =
getClientUtil().createParameterContext("TestContext", "myParam", "hello",
false);
+ final String paramContextId = paramContext.getId();
+
+ // Create a CountEvents processor and set its non-sensitive "Name"
property to reference the parameter
+ final ProcessorEntity processor =
getClientUtil().createProcessor("CountEvents");
+ getClientUtil().updateProcessorProperties(processor,
Collections.singletonMap("Name", "#{myParam}"));
+ final String processorId = processor.getId();
+
+ // Bind parameter context to root process group
+ getClientUtil().setParameterContext("root", paramContext);
+
+ // Verify processor is VALID
+ getClientUtil().waitForValidProcessor(processorId);
+ logger.info("Processor {} is VALID with non-sensitive parameter value
'hello'", processorId);
+
+ // Stop NiFi and remove the test extensions NAR to ghost the processor
+ final NiFiInstance nifiInstance = getNiFiInstance();
+ nifiInstance.stop();
+
+ final File instanceDir = nifiInstance.getInstanceDirectory();
+ final File libDir = new File(instanceDir, "lib");
+ final File narFile = findTestExtensionsNar(libDir);
+ assertNotNull(narFile, "Could not find test extensions NAR in " +
libDir.getAbsolutePath());
+
+ final Path movedNarPath =
instanceDir.toPath().resolve(narFile.getName() + ".removed");
+ Files.move(narFile.toPath(), movedNarPath,
StandardCopyOption.REPLACE_EXISTING);
+ logger.info("Moved NAR {} to {} to simulate missing extension",
narFile.getName(), movedNarPath);
+
+ // Restart NiFi without the NAR
+ nifiInstance.start();
+ setupClient();
+
+ // Verify the processor is now ghosted
+ waitFor(() -> {
+ final ProcessorEntity processorEntity =
getNifiClient().getProcessorClient().getProcessor(processorId);
+ return processorEntity.getComponent().getExtensionMissing();
+ });
+ logger.info("Processor {} is ghosted (extension missing)",
processorId);
+
+ // Update the parameter value while the processor is ghosted. Because
the processor is ghosted, all of its
+ // properties are treated as sensitive. Without the fix in
StandardParameterContext, this update would fail
+ // because the ghosted processor would appear to have a sensitive
property referencing a non-sensitive parameter.
+ final ParameterContextEntity currentParamContext =
getNifiClient().getParamContextClient().getParamContext(paramContextId, false);
+ final ParameterContextUpdateRequestEntity updateRequest =
getClientUtil().updateParameterContext(currentParamContext, "myParam", "world");
+
getClientUtil().waitForParameterContextRequestToComplete(paramContextId,
updateRequest.getRequest().getRequestId());
+ logger.info("Successfully updated parameter 'myParam' from 'hello' to
'world' while processor is ghosted");
+
+ // Stop NiFi, restore the NAR, and restart
+ nifiInstance.stop();
+ Files.move(movedNarPath, narFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);
+ logger.info("Restored NAR {} to {}", narFile.getName(),
libDir.getAbsolutePath());
+
+ nifiInstance.start();
+ setupClient();
+
+ // Verify the processor is no longer ghosted
+ waitFor(() -> {
+ final ProcessorEntity processorEntity =
getNifiClient().getProcessorClient().getProcessor(processorId);
+ return !processorEntity.getComponent().getExtensionMissing();
+ });
+ logger.info("Processor {} is no longer ghosted after NAR restore",
processorId);
+
+ // Verify the processor is VALID with the updated parameter value
+ getClientUtil().waitForValidProcessor(processorId);
+ final ProcessorEntity validProcessor =
getNifiClient().getProcessorClient().getProcessor(processorId);
+ assertFalse(validProcessor.getComponent().getExtensionMissing(),
"Processor should not be ghosted");
+ logger.info("Processor {} is VALID after NAR restore", processorId);
+
+ // Verify we can still update the parameter now that the processor is
no longer ghosted
+ final ParameterContextEntity restoredParamContext =
getNifiClient().getParamContextClient().getParamContext(paramContextId, false);
+ final ParameterContextUpdateRequestEntity secondUpdate =
getClientUtil().updateParameterContext(restoredParamContext, "myParam",
"final-value");
+
getClientUtil().waitForParameterContextRequestToComplete(paramContextId,
secondUpdate.getRequest().getRequestId());
+ logger.info("Successfully updated parameter 'myParam' to 'final-value'
after processor is no longer ghosted");
+
+ // Verify the processor is still VALID after the second parameter
update
+ getClientUtil().waitForValidProcessor(processorId);
+ }
+
+ private File findTestExtensionsNar(final File libDir) {
+ final File[] narFiles = libDir.listFiles((dir, name) ->
name.startsWith(TEST_EXTENSIONS_NAR_PREFIX) && name.endsWith(".nar"));
+ if (narFiles == null || narFiles.length == 0) {
+ return null;
+ }
+ return narFiles[0];
+ }
+}