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 14fa8a2fb03 NIFI-16286 Prevent stale Parameter Context provenance from 
breaking Context retrieval (#11618)
14fa8a2fb03 is described below

commit 14fa8a2fb03e7a11320119054bb8cc05a8202b8f
Author: Pierre Villard <[email protected]>
AuthorDate: Mon Sep 7 16:14:57 2026 +0200

    NIFI-16286 Prevent stale Parameter Context provenance from breaking Context 
retrieval (#11618)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../apache/nifi/web/StandardNiFiServiceFacade.java |  11 +-
 .../org/apache/nifi/web/api/dto/DtoFactory.java    |  16 +-
 .../web/dao/impl/StandardParameterContextDAO.java  |   4 +-
 .../nifi/web/StandardNiFiServiceFacadeTest.java    | 130 ++++++++++++++++
 .../apache/nifi/web/api/dto/DtoFactoryTest.java    |  60 ++++++++
 .../dao/impl/TestStandardParameterContextDAO.java  | 167 +++++++++++++++++++--
 .../system/parameters/ParameterContextIT.java      |  33 ++++
 7 files changed, 402 insertions(+), 19 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 786b008514a..90db89eea4d 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
@@ -1833,7 +1833,16 @@ public class StandardNiFiServiceFacade implements 
NiFiServiceFacade {
             return parameterContext;
         }
 
-        return parameterContextDAO.getParameterContext(sourceContextId);
+        if (parameterContextDAO.hasParameterContext(sourceContextId)) {
+            try {
+                return 
parameterContextDAO.getParameterContext(sourceContextId);
+            } catch (final ResourceNotFoundException ignored) {
+            }
+        }
+
+        logger.warn("Parameter [{}] in Parameter Context [{}] references 
missing source Parameter Context [{}] and is not locally owned; reporting as 
locally defined",
+                parameter.getDescriptor().getName(), 
parameterContext.getIdentifier(), sourceContextId);
+        return parameterContext;
     }
 
     private void addReferencingComponents(final ControllerServiceNode service, 
final Set<ComponentNode> affectedComponents, final List<ParameterDTO> 
affectedParameterDtos,
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
index 9fe3af8f41e..46e8ab35d7a 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/dto/DtoFactory.java
@@ -198,6 +198,7 @@ import org.apache.nifi.util.FlowDifferenceFilters;
 import org.apache.nifi.util.FormatUtils;
 import org.apache.nifi.util.security.MessageDigestUtils;
 import org.apache.nifi.web.FlowModification;
+import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.Revision;
 import 
org.apache.nifi.web.api.dto.SystemDiagnosticsSnapshotDTO.ResourceClaimDetailsDTO;
 import org.apache.nifi.web.api.dto.action.ActionDTO;
@@ -1654,8 +1655,19 @@ public final class DtoFactory {
             return fromGraph;
         }
 
-        final ParameterContext fromLookup = 
parameterContextLookup.getParameterContext(sourceId);
-        return fromLookup != null ? fromLookup : parameterContext;
+        if (parameterContextLookup.hasParameterContext(sourceId)) {
+            try {
+                final ParameterContext fromLookup = 
parameterContextLookup.getParameterContext(sourceId);
+                if (fromLookup != null) {
+                    return fromLookup;
+                }
+            } catch (final ResourceNotFoundException ignored) {
+            }
+        }
+
+        logger.warn("Parameter [{}] in Parameter Context [{}] references 
missing source Parameter Context [{}]; reporting as locally defined",
+                parameter.getDescriptor().getName(), 
parameterContext.getIdentifier(), sourceId);
+        return parameterContext;
     }
 
     private ParameterContext findInheritedParameterContext(final 
ParameterContext parameterContext, final String sourceId, final Set<String> 
visited) {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
index 090368245eb..71acf72efab 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
@@ -222,8 +222,6 @@ public class StandardParameterContextDAO implements 
ParameterContextDAO {
         final String dtoValue = dto.getValue();
         final List<AssetReferenceDTO> referencedAssets = 
dto.getReferencedAssets();
         final boolean referencesAsset = referencedAssets != null && 
!referencedAssets.isEmpty();
-        final String parameterContextId = dto.getParameterContext() == null ? 
null : dto.getParameterContext().getId();
-
         final String value;
         List<Asset> assets = null;
         if (dtoValue == null && !referencesAsset && 
Boolean.TRUE.equals(dto.getValueRemoved())) {
@@ -245,7 +243,7 @@ public class StandardParameterContextDAO implements 
ParameterContextDAO {
             .name(dto.getName())
             .description(dto.getDescription())
             .sensitive(Boolean.TRUE.equals(dto.getSensitive()))
-            .parameterContextId(parameterContextId)
+            .parameterContextId(null)
             .value(value)
             .referencedAssets(assets)
             .provided(dto.getProvided())
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 97f7d20ca0b..079f8cf8629 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
@@ -2531,6 +2531,7 @@ public class StandardNiFiServiceFacadeTest {
         final ParameterContextDAO parameterContextDAO = 
mock(ParameterContextDAO.class);
         
when(parameterContextDAO.getParameterContext(targetContextId)).thenReturn(targetContext);
         
when(parameterContextDAO.getParameterContext(inheritedContextId)).thenReturn(inheritedContext);
+        
when(parameterContextDAO.hasParameterContext(inheritedContextId)).thenReturn(true);
         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))))
@@ -2623,6 +2624,134 @@ public class StandardNiFiServiceFacadeTest {
         assertFalse(secondPassParameters.containsKey(aliasParameterName));
     }
 
+    @Test
+    public void 
testGetComponentsAffectedByParameterContextUpdateTwiceFallsBackWhenSourceContextDisappears()
 {
+        final String targetContextId = "target-context";
+        final String inheritedContextId = "inherited-context";
+        final String inheritedParameterName = "inherited-provider-param";
+        final String inheritedParameterValue = "provider-secret-value";
+        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 Parameter maskedInheritedParameter = new Parameter.Builder()
+                .descriptor(new 
ParameterDescriptor.Builder().name(inheritedParameterName).sensitive(true).build())
+                .value(inheritedParameterValue)
+                .provided(true)
+                .parameterContextId(inheritedContextId)
+                .build();
+
+        final ParameterContext inheritedContext = mock(ParameterContext.class);
+        when(inheritedContext.getIdentifier()).thenReturn(inheritedContextId);
+        when(inheritedContext.getName()).thenReturn("Inherited Context");
+        
when(inheritedContext.getInheritedParameterContexts()).thenReturn(List.of());
+
+        final ParameterContext targetContext = mock(ParameterContext.class);
+        when(targetContext.getIdentifier()).thenReturn(targetContextId);
+        when(targetContext.getName()).thenReturn("Target Context");
+        when(targetContext.getParameters()).thenReturn(Map.of());
+        
when(targetContext.getParameterReferenceManager()).thenReturn(ParameterReferenceManager.EMPTY);
+        
when(targetContext.getInheritedParameterContexts()).thenReturn(List.of(inheritedContext));
+
+        final ParameterContextDAO parameterContextDAO = 
mock(ParameterContextDAO.class);
+        
when(parameterContextDAO.getParameterContext(targetContextId)).thenReturn(targetContext);
+        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))
+                .thenReturn(Map.of(inheritedParameterName, 
maskedInheritedParameter));
+        
when(parameterContextDAO.hasParameterContext(inheritedContextId)).thenReturn(true,
 true);
+        when(parameterContextDAO.getParameterContext(inheritedContextId))
+                .thenReturn(inheritedContext)
+                .thenThrow(new ResourceNotFoundException("Source context was 
removed"));
+
+        final ProcessorNode processorNode = mock(ProcessorNode.class);
+        when(processorNode.isRunning()).thenReturn(true);
+        
when(processorNode.getReferencedParameterNames()).thenReturn(Set.of(inheritedParameterName));
+        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 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);
+        assertTrue(firstPassParameter.getInherited());
+        assertTrue(firstPassParameter.getProvided());
+        assertEquals(inheritedContextId, 
firstPassParameter.getParameterContext().getId());
+        assertEquals(inheritedParameterValue, firstPassParameter.getValue());
+
+        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);
+        assertFalse(secondPassParameter.getInherited());
+        assertTrue(secondPassParameter.getProvided());
+        assertEquals(targetContextId, 
secondPassParameter.getParameterContext().getId());
+        assertEquals(inheritedParameterValue, secondPassParameter.getValue());
+        assertEquals(1, secondPassParameter.getReferencingComponents().size());
+        assertEquals(processorId, 
secondPassParameter.getReferencingComponents().iterator().next().getId());
+
+        verify(parameterContextDAO, 
times(2)).hasParameterContext(inheritedContextId);
+        verify(parameterContextDAO, 
times(2)).getParameterContext(inheritedContextId);
+    }
+
     @Test
     public void 
testGetComponentsAffectedByParameterContextUpdateDoesNotAddSensitiveLocalAliasToUpdate()
 {
         final String targetContextId = "target-context";
@@ -3154,4 +3283,5 @@ public class StandardNiFiServiceFacadeTest {
         serviceFacade.setParameterContextDAO(parameterContextDAO);
         return assetManager;
     }
+
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
index b8d84fb4d6b..a6f4fb1cd81 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/dto/DtoFactoryTest.java
@@ -50,6 +50,7 @@ import org.apache.nifi.processor.Relationship;
 import org.apache.nifi.registry.flow.FlowRegistryClientNode;
 import org.apache.nifi.registry.flow.diff.DifferenceType;
 import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.web.ResourceNotFoundException;
 import org.apache.nifi.web.api.entity.AllowableValueEntity;
 import org.apache.nifi.web.api.entity.ParameterContextReferenceEntity;
 import org.apache.nifi.web.revision.RevisionManager;
@@ -878,6 +879,7 @@ public class DtoFactoryTest {
                 .build();
 
         final ParameterContextLookup lookup = 
mock(ParameterContextLookup.class);
+        when(lookup.hasParameterContext(externalId)).thenReturn(true);
         
when(lookup.getParameterContext(externalId)).thenReturn(externalContext);
 
         final DtoFactory dtoFactory = newDtoFactoryForParameters();
@@ -886,6 +888,7 @@ public class DtoFactoryTest {
         assertTrue(dto.getInherited());
         assertEquals(externalId, dto.getParameterContext().getId());
 
+        verify(lookup).hasParameterContext(externalId);
         verify(lookup).getParameterContext(externalId);
     }
 
@@ -909,6 +912,60 @@ public class DtoFactoryTest {
         assertEquals(contextId, dto.getParameterContext().getId());
     }
 
+    @Test
+    void 
testCreateParameterDtoFallsBackToCurrentContextWhenLookupReportsMissingSourceWithoutCallingGetter()
 {
+        final String contextId = "context-1";
+        final String missingSourceId = "context-missing";
+        final String parameterName = "param-name";
+
+        final ParameterContext parameterContext = 
createMockParameterContext(contextId, "context-1-name", 
Collections.emptyList());
+        final Parameter parameter = new Parameter.Builder()
+                .name(parameterName)
+                .value("param-value")
+                .parameterContextId(missingSourceId)
+                .build();
+
+        final ParameterContextLookup lookup = 
mock(ParameterContextLookup.class);
+        when(lookup.hasParameterContext(missingSourceId)).thenReturn(false);
+        when(lookup.getParameterContext(missingSourceId)).thenThrow(new 
AssertionError("Lookup getter should not be called for a missing source 
context"));
+
+        final DtoFactory dtoFactory = newDtoFactoryForParameters();
+        final ParameterDTO dto = 
dtoFactory.createParameterDto(parameterContext, parameter, 
mock(RevisionManager.class), lookup);
+
+        assertFalse(dto.getInherited());
+        assertEquals(contextId, dto.getParameterContext().getId());
+
+        verify(lookup).hasParameterContext(missingSourceId);
+        verify(lookup, never()).getParameterContext(missingSourceId);
+    }
+
+    @Test
+    void 
testCreateParameterDtoFallsBackToCurrentContextWhenSourceDisappearsDuringLookup()
 {
+        final String contextId = "context-1";
+        final String missingSourceId = "context-missing";
+        final String parameterName = "param-name";
+
+        final ParameterContext parameterContext = 
createMockParameterContext(contextId, "context-1-name", 
Collections.emptyList());
+        final Parameter parameter = new Parameter.Builder()
+                .name(parameterName)
+                .value("param-value")
+                .parameterContextId(missingSourceId)
+                .build();
+
+        final ParameterContextLookup lookup = 
mock(ParameterContextLookup.class);
+        when(lookup.hasParameterContext(missingSourceId)).thenReturn(true);
+        when(lookup.getParameterContext(missingSourceId)).thenThrow(new 
ResourceNotFoundException("Source context was removed"));
+
+        final DtoFactory dtoFactory = newDtoFactoryForParameters();
+        final ParameterDTO dto = 
dtoFactory.createParameterDto(parameterContext, parameter, 
mock(RevisionManager.class), lookup);
+
+        assertFalse(dto.getInherited());
+        assertEquals(contextId, dto.getParameterContext().getId());
+
+        verify(lookup).hasParameterContext(missingSourceId);
+        verify(lookup).getParameterContext(missingSourceId);
+    }
+
     @Test
     void 
testCreateParameterDtoResolvesSourceContextFromDiamondInheritanceGraph() {
         final String contextAId = "context-a";
@@ -955,6 +1012,7 @@ public class DtoFactoryTest {
 
         final ParameterContext fallbackContext = 
createMockParameterContext(missingId, "missing", Collections.emptyList());
         final ParameterContextLookup lookup = 
mock(ParameterContextLookup.class);
+        when(lookup.hasParameterContext(missingId)).thenReturn(true);
         
when(lookup.getParameterContext(missingId)).thenReturn(fallbackContext);
 
         final DtoFactory dtoFactory = newDtoFactoryForParameters();
@@ -963,6 +1021,7 @@ public class DtoFactoryTest {
         assertTrue(dto.getInherited());
         assertEquals(missingId, dto.getParameterContext().getId());
 
+        verify(lookup).hasParameterContext(missingId);
         verify(lookup).getParameterContext(missingId);
     }
 
@@ -1002,4 +1061,5 @@ public class DtoFactoryTest {
         when(context.getName()).thenReturn(name);
         
when(context.getParameterReferenceManager()).thenReturn(ParameterReferenceManager.EMPTY);
     }
+
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/dao/impl/TestStandardParameterContextDAO.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/dao/impl/TestStandardParameterContextDAO.java
index 60531f909d7..e05019cca0d 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/dao/impl/TestStandardParameterContextDAO.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/dao/impl/TestStandardParameterContextDAO.java
@@ -17,6 +17,8 @@
 
 package org.apache.nifi.web.dao.impl;
 
+import org.apache.nifi.asset.Asset;
+import org.apache.nifi.asset.AssetManager;
 import org.apache.nifi.authorization.AuthorizationRequest;
 import org.apache.nifi.authorization.AuthorizationResult;
 import org.apache.nifi.authorization.Authorizer;
@@ -31,6 +33,7 @@ import org.apache.nifi.parameter.ParameterReferenceManager;
 import org.apache.nifi.parameter.StandardParameterContext;
 import org.apache.nifi.parameter.StandardParameterContextManager;
 import org.apache.nifi.parameter.StandardParameterReferenceManager;
+import org.apache.nifi.web.api.dto.AssetReferenceDTO;
 import org.apache.nifi.web.api.dto.ParameterContextDTO;
 import org.apache.nifi.web.api.dto.ParameterContextReferenceDTO;
 import org.apache.nifi.web.api.dto.ParameterDTO;
@@ -46,25 +49,43 @@ import 
org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+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.mockito.Answers.RETURNS_DEEP_STUBS;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 public class TestStandardParameterContextDAO {
 
+    private static final String CONTEXT_ID = "id";
+    private static final String CONTEXT_NAME = "Context";
+    private static final String INHERITED_CONTEXT_ID = "inherited-id";
+
     private StandardParameterContextDAO dao;
 
     @Mock(answer = RETURNS_DEEP_STUBS)
     private FlowController flowController;
 
+    @Mock
+    private AssetManager assetManager;
+
     @Mock
     private Authentication authentication;
 
@@ -74,26 +95,27 @@ public class TestStandardParameterContextDAO {
     @BeforeEach
     void setUp() {
         dao = new StandardParameterContextDAO();
+        when(flowController.getAssetManager()).thenReturn(assetManager);
         dao.setFlowController(flowController);
         dao.setAuthorizer(authorizer);
-        
when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
+        
lenient().when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
 
         final SecurityContext securityContext = 
SecurityContextHolder.getContext();
         securityContext.setAuthentication(authentication);
         final NiFiUser user = new 
StandardNiFiUser.Builder().identity("user").build();
         final NiFiUserDetails userDetail = new NiFiUserDetails(user);
-        when(authentication.getPrincipal()).thenReturn(userDetail);
+        lenient().when(authentication.getPrincipal()).thenReturn(userDetail);
 
         final ParameterReferenceManager parameterReferenceManager = new 
StandardParameterReferenceManager(() -> 
flowController.getFlowManager().getRootGroup());
 
         final FlowManager flowManager = flowController.getFlowManager();
         final StandardParameterContextManager parameterContextLookup = new 
StandardParameterContextManager();
         
when(flowManager.getParameterContextManager()).thenReturn(parameterContextLookup);
-        parameterContextLookup.addParameterContext(new 
StandardParameterContext.Builder().id("id")
-                .name("Context")
+        parameterContextLookup.addParameterContext(new 
StandardParameterContext.Builder().id(CONTEXT_ID)
+                .name(CONTEXT_NAME)
                 .parameterReferenceManager(parameterReferenceManager)
                 .build());
-        final ParameterContext inheritedContext = new 
StandardParameterContext.Builder().id("inherited-id")
+        final ParameterContext inheritedContext = new 
StandardParameterContext.Builder().id(INHERITED_CONTEXT_ID)
                 .parameterReferenceManager(parameterReferenceManager)
                 .name("Inherited")
                 .build();
@@ -106,14 +128,14 @@ public class TestStandardParameterContextDAO {
     @Test
     public void testVerifyUpdateInheritedProvidedParameter() {
         final ParameterContextDTO dto = new ParameterContextDTO();
-        dto.setId("id");
-        dto.setName("Context");
+        dto.setId(CONTEXT_ID);
+        dto.setName(CONTEXT_NAME);
 
         final List<ParameterContextReferenceEntity> refs = new ArrayList<>();
         final ParameterContextReferenceEntity ref = new 
ParameterContextReferenceEntity();
-        ref.setId("inherited-id");
+        ref.setId(INHERITED_CONTEXT_ID);
         ref.setComponent(new ParameterContextReferenceDTO());
-        ref.getComponent().setId("inherited-id");
+        ref.getComponent().setId(INHERITED_CONTEXT_ID);
         ref.getComponent().setName("Inherited");
         refs.add(ref);
         dto.setInheritedParameterContexts(refs);
@@ -125,8 +147,8 @@ public class TestStandardParameterContextDAO {
     @Test
     public void testVerifyUpdateNonInheritedProvidedParameter() {
         final ParameterContextDTO dto = new ParameterContextDTO();
-        dto.setId("id");
-        dto.setName("Context");
+        dto.setId(CONTEXT_ID);
+        dto.setName(CONTEXT_NAME);
         final Set<ParameterEntity> parameters = new HashSet<>();
         final ParameterEntity parameter = new ParameterEntity();
         parameter.setCanWrite(true);
@@ -141,9 +163,9 @@ public class TestStandardParameterContextDAO {
 
         final List<ParameterContextReferenceEntity> refs = new ArrayList<>();
         final ParameterContextReferenceEntity ref = new 
ParameterContextReferenceEntity();
-        ref.setId("inherited-id");
+        ref.setId(INHERITED_CONTEXT_ID);
         ref.setComponent(new ParameterContextReferenceDTO());
-        ref.getComponent().setId("inherited-id");
+        ref.getComponent().setId(INHERITED_CONTEXT_ID);
         ref.getComponent().setName("Inherited");
         refs.add(ref);
         dto.setInheritedParameterContexts(refs);
@@ -151,4 +173,123 @@ public class TestStandardParameterContextDAO {
         // Updating a provided parameter that is not inherited should fail
         assertThrows(IllegalArgumentException.class, () -> 
dao.verifyUpdate(dto, true));
     }
+
+    @Test
+    public void testGetParametersNormalizesNullSourceForLocalParameter() {
+        final ParameterEntity parameterEntity = 
createParameterEntity("param-null-source", "value-1", false, "description-1", 
false, null, null);
+
+        final Map<String, Parameter> parameters = 
dao.getParameters(createParameterContextDto(parameterEntity), null);
+
+        final Parameter parameter = parameters.get("param-null-source");
+        assertEquals("value-1", parameter.getValue());
+        assertEquals("description-1", 
parameter.getDescriptor().getDescription());
+        assertFalse(parameter.getDescriptor().isSensitive());
+        assertFalse(parameter.isProvided());
+        assertNull(parameter.getParameterContextId());
+    }
+
+    @Test
+    public void 
testGetParametersNormalizesCurrentSourceForExistingLocalParameterUpdate() {
+        final ParameterContext context = dao.getParameterContext(CONTEXT_ID);
+        context.setParameters(Map.of(
+                "param-current-source",
+                new 
Parameter.Builder().name("param-current-source").value("existing-value").build()
+        ));
+
+        final ParameterEntity parameterEntity = 
createParameterEntity("param-current-source", null, true, "description-2", 
false, CONTEXT_ID, null);
+
+        final Map<String, Parameter> parameters = 
dao.getParameters(createParameterContextDto(parameterEntity), context);
+
+        final Parameter parameter = parameters.get("param-current-source");
+        assertEquals("existing-value", parameter.getValue());
+        assertEquals("description-2", 
parameter.getDescriptor().getDescription());
+        assertTrue(parameter.getDescriptor().isSensitive());
+        assertFalse(parameter.isProvided());
+        assertNull(parameter.getParameterContextId());
+    }
+
+    @Test
+    public void testGetParametersNormalizesForeignSourceAndPreservesFields() {
+        final ParameterEntity parameterEntity = 
createParameterEntity("param-foreign-source", "value-3", true, "description-3", 
false, "foreign-context", null);
+
+        final Map<String, Parameter> parameters = 
dao.getParameters(createParameterContextDto(parameterEntity), null);
+
+        final Parameter parameter = parameters.get("param-foreign-source");
+        assertEquals("value-3", parameter.getValue());
+        assertEquals("description-3", 
parameter.getDescriptor().getDescription());
+        assertTrue(parameter.getDescriptor().isSensitive());
+        assertFalse(parameter.isProvided());
+        assertNull(parameter.getParameterContextId());
+    }
+
+    @Test
+    public void testGetParametersNormalizesForeignSourceForProvidedParameter() 
{
+        final ParameterEntity parameterEntity = 
createParameterEntity("provided-param", "provided-value", false, 
"provided-description", true, "foreign-context", null);
+
+        final Map<String, Parameter> parameters = 
dao.getParameters(createParameterContextDto(parameterEntity), null);
+
+        final Parameter parameter = parameters.get("provided-param");
+        assertEquals("provided-value", parameter.getValue());
+        assertEquals("provided-description", 
parameter.getDescriptor().getDescription());
+        assertFalse(parameter.getDescriptor().isSensitive());
+        assertTrue(parameter.isProvided());
+        assertNull(parameter.getParameterContextId());
+    }
+
+    @Test
+    public void 
testGetParametersNormalizesForeignSourceForAssetBackedParameterAndPassesOwnershipValidation()
 {
+        final Asset asset = mock(Asset.class);
+        when(asset.getOwnerIdentifier()).thenReturn(CONTEXT_ID);
+        when(asset.getFile()).thenReturn(new java.io.File("asset.bin"));
+        when(assetManager.getAsset("asset-1")).thenReturn(Optional.of(asset));
+
+        final AssetReferenceDTO assetReference = new AssetReferenceDTO();
+        assetReference.setId("asset-1");
+
+        final ParameterEntity parameterEntity = 
createParameterEntity("asset-param", "client-value-ignored", false, 
"asset-description", false,
+                "foreign-context", List.of(assetReference));
+        final ParameterContextDTO parameterContextDto = 
createParameterContextDto(parameterEntity);
+
+        final Map<String, Parameter> parameters = 
dao.getParameters(parameterContextDto, null);
+        final Parameter parameter = parameters.get("asset-param");
+
+        assertEquals(asset.getFile().getAbsolutePath(), parameter.getValue());
+        assertEquals("asset-description", 
parameter.getDescriptor().getDescription());
+        assertNull(parameter.getParameterContextId());
+        assertEquals(Collections.singletonList(asset), 
parameter.getReferencedAssets());
+        assertDoesNotThrow(() -> dao.verifyAssets(parameterContextDto, 
parameters));
+        verify(assetManager).getAsset(eq("asset-1"));
+    }
+
+    private ParameterContextDTO createParameterContextDto(final 
ParameterEntity... parameterEntities) {
+        final ParameterContextDTO parameterContextDto = new 
ParameterContextDTO();
+        parameterContextDto.setId(CONTEXT_ID);
+        parameterContextDto.setName(CONTEXT_NAME);
+        parameterContextDto.setParameters(new 
HashSet<>(List.of(parameterEntities)));
+        return parameterContextDto;
+    }
+
+    private ParameterEntity createParameterEntity(final String name, final 
String value, final Boolean sensitive, final String description,
+                                                  final Boolean provided, 
final String sourceContextId, final List<AssetReferenceDTO> referencedAssets) {
+        final ParameterDTO parameterDto = new ParameterDTO();
+        parameterDto.setName(name);
+        parameterDto.setValue(value);
+        parameterDto.setSensitive(sensitive);
+        parameterDto.setDescription(description);
+        parameterDto.setProvided(provided);
+        parameterDto.setReferencedAssets(referencedAssets);
+        if (sourceContextId != null) {
+            final ParameterContextReferenceEntity parameterContextReference = 
new ParameterContextReferenceEntity();
+            parameterContextReference.setId(sourceContextId);
+            final ParameterContextReferenceDTO parameterContextReferenceDto = 
new ParameterContextReferenceDTO();
+            parameterContextReferenceDto.setId(sourceContextId);
+            
parameterContextReference.setComponent(parameterContextReferenceDto);
+            parameterDto.setParameterContext(parameterContextReference);
+        }
+
+        final ParameterEntity parameterEntity = new ParameterEntity();
+        parameterEntity.setCanWrite(true);
+        parameterEntity.setParameter(parameterDto);
+        return parameterEntity;
+    }
 }
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 78b7e6e129e..e68c3dc71ca 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
@@ -33,6 +33,7 @@ import org.apache.nifi.web.api.entity.ConnectionEntity;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
 import org.apache.nifi.web.api.entity.ParameterContextEntity;
 import org.apache.nifi.web.api.entity.ParameterContextUpdateRequestEntity;
+import org.apache.nifi.web.api.entity.ParameterContextsEntity;
 import org.apache.nifi.web.api.entity.ParameterEntity;
 import org.apache.nifi.web.api.entity.ParameterGroupConfigurationEntity;
 import 
org.apache.nifi.web.api.entity.ParameterProviderApplyParametersRequestEntity;
@@ -1168,6 +1169,38 @@ public class ParameterContextIT extends NiFiSystemIT {
         final ParameterContextEntity updatedParent = 
getNifiClient().getParamContextClient().getParamContext(parentContext.getId(), 
false);
         assertEquals(1, 
updatedParent.getComponent().getInheritedParameterContexts().size());
         assertEquals(childContext2.getId(), 
updatedParent.getComponent().getInheritedParameterContexts().get(0).getId());
+
+        final ParameterContextEntity deletedChildContext = 
getNifiClient().getParamContextClient().getParamContext(childContext1.getId(), 
false);
+        
getNifiClient().getParamContextClient().deleteParamContext(childContext1.getId(),
 String.valueOf(deletedChildContext.getRevision().getVersion()));
+
+        final ParameterContextsEntity parameterContexts = 
getNifiClient().getParamContextClient().getParamContexts();
+        final ParameterContextEntity listedParent = 
parameterContexts.getParameterContexts().stream()
+                .filter(context -> 
parentContext.getId().equals(context.getId()))
+                .findFirst()
+                .orElse(null);
+        assertNotNull(listedParent);
+        
assertTrue(parameterContexts.getParameterContexts().stream().anyMatch(context 
-> childContext2.getId().equals(context.getId())));
+        
assertFalse(parameterContexts.getParameterContexts().stream().anyMatch(context 
-> childContext1.getId().equals(context.getId())));
+        assertEquals(1, 
listedParent.getComponent().getInheritedParameterContexts().size());
+        assertEquals(childContext2.getId(), 
listedParent.getComponent().getInheritedParameterContexts().getFirst().getId());
+
+        final ParameterContextEntity survivingParentLocal = 
getNifiClient().getParamContextClient().getParamContext(parentContext.getId(), 
false);
+        assertTrue(survivingParentLocal.getComponent().getParameters().stream()
+                .noneMatch(parameter -> 
"fileToIngest".equals(parameter.getParameter().getName())));
+
+        final ParameterContextEntity survivingParent = 
getNifiClient().getParamContextClient().getParamContext(parentContext.getId(), 
true);
+        assertEquals(1, 
survivingParent.getComponent().getInheritedParameterContexts().size());
+        assertEquals(childContext2.getId(), 
survivingParent.getComponent().getInheritedParameterContexts().getFirst().getId());
+        final ParameterDTO survivingParameter = 
survivingParent.getComponent().getParameters().stream()
+                .map(ParameterEntity::getParameter)
+                .filter(parameter -> "otherParam".equals(parameter.getName()))
+                .findFirst()
+                .orElse(null);
+        assertNotNull(survivingParameter);
+        assertTrue(survivingParameter.getInherited());
+        assertEquals(childContext2.getId(), 
survivingParameter.getParameterContext().getId());
+        assertTrue(survivingParent.getComponent().getParameters().stream()
+                .noneMatch(parameter -> 
"fileToIngest".equals(parameter.getParameter().getName())));
     }
 
     @Test

Reply via email to