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];
+    }
+}

Reply via email to