From d2c131caee1600ec48e334f8071fe4aede3ada5d Mon Sep 17 00:00:00 2001 From: Pierre Villard Date: Thu, 3 Sep 2026 12:35:58 +0200 Subject: [PATCH] NIFI-16286 - Prevent stale Parameter Context provenance from breaking context retrieval --- .../nifi/web/StandardNiFiServiceFacade.java | 11 +- .../apache/nifi/web/api/dto/DtoFactory.java | 16 +- .../dao/impl/StandardParameterContextDAO.java | 4 +- .../web/StandardNiFiServiceFacadeTest.java | 164 +++++++++++++++++ .../nifi/web/api/dto/DtoFactoryTest.java | 108 +++++++++++ .../impl/TestStandardParameterContextDAO.java | 167 ++++++++++++++++-- .../system/parameters/ParameterContextIT.java | 33 ++++ 7 files changed, 484 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 4b9e9b2fface..b7f927d49245 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 @@ -1838,7 +1838,16 @@ private ParameterContext getContainingParameterContext(final ParameterContext pa 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 affectedComponents, final List 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 9fe3af8f41ee..46e8ab35d7ad 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.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 @@ private ParameterContext resolveContainingParameterContext(final ParameterContex 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 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 090368245eb4..71acf72efab7 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 @@ private Parameter createParameter(final ParameterDTO dto, final ParameterContext final String dtoValue = dto.getValue(); final List referencedAssets = dto.getReferencedAssets(); final boolean referencesAsset = referencedAssets != null && !referencedAssets.isEmpty(); - final String parameterContextId = dto.getParameterContext() == null ? null : dto.getParameterContext().getId(); - final String value; List assets = null; if (dtoValue == null && !referencesAsset && Boolean.TRUE.equals(dto.getValueRemoved())) { @@ -245,7 +243,7 @@ private Parameter createParameter(final ParameterDTO dto, final ParameterContext .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 97f7d20ca0b1..a94405efd4a8 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 @@ -193,8 +193,10 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.io.PrintStream; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; @@ -2531,6 +2533,7 @@ public void testGetComponentsAffectedByParameterContextUpdateTwicePreservesParam 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 +2626,145 @@ public void testGetComponentsAffectedByParameterContextUpdateTwicePreservesParam 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 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); + + captureStandardError(() -> { + final Set firstAffected = serviceFacade.getComponentsAffectedByParameterContextUpdate(List.of(parameterContextDto)); + assertEquals(1, firstAffected.size()); + assertEquals(processorId, firstAffected.iterator().next().getId()); + + final Map 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 secondAffected = serviceFacade.getComponentsAffectedByParameterContextUpdate(List.of(parameterContextDto)); + assertEquals(1, secondAffected.size()); + assertEquals(processorId, secondAffected.iterator().next().getId()); + + final Map 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); + }, standardError -> { + final String warningMessage = standardError; + assertFalse(warningMessage.isEmpty()); + assertTrue(warningMessage.contains("not locally owned")); + assertTrue(warningMessage.contains(inheritedParameterName)); + assertTrue(warningMessage.contains(targetContextId)); + assertTrue(warningMessage.contains(inheritedContextId)); + assertFalse(warningMessage.contains(inheritedParameterValue)); + assertTrue(standardError.toLowerCase().contains("warn")); + }); + } + @Test public void testGetComponentsAffectedByParameterContextUpdateDoesNotAddSensitiveLocalAliasToUpdate() { final String targetContextId = "target-context"; @@ -3154,4 +3296,26 @@ private AssetManager configureAssets(final Asset asset, final ParameterContext p serviceFacade.setParameterContextDAO(parameterContextDAO); return assetManager; } + + private void captureStandardError(final ThrowingRunnable action, final java.util.function.Consumer assertions) { + synchronized (System.class) { + final PrintStream originalError = System.err; + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (PrintStream capture = new PrintStream(outputStream, true, StandardCharsets.UTF_8)) { + System.setErr(capture); + action.run(); + } catch (final Exception e) { + throw new RuntimeException(e); + } finally { + System.setErr(originalError); + } + + assertions.accept(outputStream.toString(StandardCharsets.UTF_8)); + } + } + + @FunctionalInterface + private interface ThrowingRunnable { + void run() throws Exception; + } } 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 b8d84fb4d6bf..160c25443478 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.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; @@ -57,7 +58,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -878,6 +882,7 @@ void testCreateParameterDtoFallsBackToLookupWhenSourceNotReachableInGraph() { .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 +891,7 @@ void testCreateParameterDtoFallsBackToLookupWhenSourceNotReachableInGraph() { assertTrue(dto.getInherited()); assertEquals(externalId, dto.getParameterContext().getId()); + verify(lookup).hasParameterContext(externalId); verify(lookup).getParameterContext(externalId); } @@ -909,6 +915,84 @@ void testCreateParameterDtoFallsBackToCurrentContextWhenSourceNotReachableInGrap assertEquals(contextId, dto.getParameterContext().getId()); } + @Test + void testCreateParameterDtoFallsBackToCurrentContextWhenLookupReportsMissingSourceWithoutCallingGetter() { + final String contextId = "context-1"; + final String missingSourceId = "context-missing"; + final String parameterName = "param-name"; + final String parameterValue = "sensitive-to-logs"; + + final ParameterContext parameterContext = createMockParameterContext(contextId, "context-1-name", Collections.emptyList()); + final Parameter parameter = new Parameter.Builder() + .name(parameterName) + .value(parameterValue) + .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(); + captureStandardError(() -> { + 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); + + return null; + }, standardError -> { + assertFalse(standardError.isEmpty()); + assertTrue(standardError.contains(parameterName)); + assertTrue(standardError.contains(contextId)); + assertTrue(standardError.contains(missingSourceId)); + assertFalse(standardError.contains(parameterValue)); + assertTrue(standardError.toLowerCase().contains("warn")); + }); + } + + @Test + void testCreateParameterDtoFallsBackToCurrentContextWhenSourceDisappearsDuringLookup() { + final String contextId = "context-1"; + final String missingSourceId = "context-missing"; + final String parameterName = "param-name"; + final String parameterValue = "sensitive-to-logs"; + + final ParameterContext parameterContext = createMockParameterContext(contextId, "context-1-name", Collections.emptyList()); + final Parameter parameter = new Parameter.Builder() + .name(parameterName) + .value(parameterValue) + .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(); + captureStandardError(() -> { + 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); + + return null; + }, standardError -> { + assertFalse(standardError.isEmpty()); + assertTrue(standardError.contains(parameterName)); + assertTrue(standardError.contains(contextId)); + assertTrue(standardError.contains(missingSourceId)); + assertFalse(standardError.contains(parameterValue)); + assertTrue(standardError.toLowerCase().contains("warn")); + }); + } + @Test void testCreateParameterDtoResolvesSourceContextFromDiamondInheritanceGraph() { final String contextAId = "context-a"; @@ -955,6 +1039,7 @@ void testCreateParameterDtoInheritanceGraphHandlesCycles() { 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 +1048,7 @@ void testCreateParameterDtoInheritanceGraphHandlesCycles() { assertTrue(dto.getInherited()); assertEquals(missingId, dto.getParameterContext().getId()); + verify(lookup).hasParameterContext(missingId); verify(lookup).getParameterContext(missingId); } @@ -1002,4 +1088,26 @@ private static void configureBaseParameterContext(final ParameterContext context when(context.getName()).thenReturn(name); when(context.getParameterReferenceManager()).thenReturn(ParameterReferenceManager.EMPTY); } + + private static void captureStandardError(final ThrowingSupplier action, final java.util.function.Consumer assertions) { + synchronized (System.class) { + final PrintStream originalError = System.err; + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + try (PrintStream capture = new PrintStream(outputStream, true, StandardCharsets.UTF_8)) { + System.setErr(capture); + action.get(); + } catch (final Exception e) { + throw new RuntimeException(e); + } finally { + System.setErr(originalError); + } + + assertions.accept(outputStream.toString(StandardCharsets.UTF_8)); + } + } + + @FunctionalInterface + private interface ThrowingSupplier { + T get() throws Exception; + } } 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 60531f909d70..e05019cca0d1 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.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.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 @@ void setUp() { @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 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 void testVerifyUpdateInheritedProvidedParameter() { @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 parameters = new HashSet<>(); final ParameterEntity parameter = new ParameterEntity(); parameter.setCanWrite(true); @@ -141,9 +163,9 @@ public void testVerifyUpdateNonInheritedProvidedParameter() { final List 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 void testVerifyUpdateNonInheritedProvidedParameter() { // 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 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 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 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 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 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 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 78b7e6e129e8..e68c3dc71ca3 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.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 void testRemoveInheritedContextWithAssetReference() throws NiFiClientExce 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