From dc3093822bb176aec0020ba27ab651e328385d71 Mon Sep 17 00:00:00 2001 From: exceptionfactory Date: Fri, 18 Sep 2026 11:53:57 -0500 Subject: [PATCH] NIFI-16366 Improved validation for CS referencing components updates --- .../authorization/AuthorizableLookup.java | 10 + ...ontrollerServiceReferencingComponents.java | 90 +++++++++ .../StandardAuthorizableLookup.java | 6 + .../web/api/ControllerServiceResource.java | 17 +- ...ollerServiceReferencingComponentsTest.java | 189 ++++++++++++++++++ .../StandardAuthorizableLookupTest.java | 23 +++ 6 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponents.java create mode 100644 nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponentsTest.java diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java index 418d21655949..0da07b2a76df 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java @@ -22,6 +22,7 @@ import org.apache.nifi.parameter.ParameterContext; import org.apache.nifi.web.api.dto.BundleDTO; +import java.util.List; import java.util.Set; import java.util.function.Predicate; @@ -213,6 +214,15 @@ public interface AuthorizableLookup { */ Authorizable getControllerServiceReferencingComponent(String controllerServiceId, String id); + /** + * Get the authorizables for components of the requested type that reference the specified Controller Service + * + * @param controllerServiceId controller service id + * @param componentType type of referencing component to be returned + * @return referencing component authorizables + */ + List getControllerServiceReferencingComponents(String controllerServiceId, Class componentType); + /** * Get the authorizable ReportingTask. * diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponents.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponents.java new file mode 100644 index 000000000000..d8dceb6e5f25 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponents.java @@ -0,0 +1,90 @@ +/* + * 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.authorization; + +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.resource.OperationAuthorizable; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.controller.FlowAnalysisRuleNode; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.ScheduledState; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceState; + +import java.util.List; + +/** + * Authorizes updates to components that reference a Controller Service. + */ +public final class AuthorizeControllerServiceReferencingComponents { + + /** + * Authorizes operation of the Controller Service together with each component that the requested state change + * affects. Referencing Controller Services are authorized when a Controller Service state is requested. Referencing + * Processors, Reporting Tasks, and Flow Analysis Rules are authorized when a scheduled state is requested. The + * referencing components are resolved from the Controller Service reference graph, matching the components that the + * requested state change updates. + * + * @param authorizer authorizer + * @param lookup lookup + * @param controllerServiceId controller service id + * @param controllerServiceState requested Controller Service state or null when not requested + * @param scheduledState requested scheduled state or null when not requested + * @param user user + */ + public static void authorize( + final Authorizer authorizer, + final AuthorizableLookup lookup, + final String controllerServiceId, + final ControllerServiceState controllerServiceState, + final ScheduledState scheduledState, + final NiFiUser user) { + + final Authorizable controllerService = lookup.getControllerService(controllerServiceId).getAuthorizable(); + OperationAuthorizable.authorizeOperation(controllerService, authorizer, user); + + if (controllerServiceState != null) { + authorizeReferencingComponents(authorizer, lookup, controllerServiceId, user, ControllerServiceNode.class); + return; + } + + if (scheduledState == null) { + return; + } + + authorizeReferencingComponents(authorizer, lookup, controllerServiceId, user, ProcessorNode.class); + authorizeReferencingComponents(authorizer, lookup, controllerServiceId, user, ReportingTaskNode.class); + authorizeReferencingComponents(authorizer, lookup, controllerServiceId, user, FlowAnalysisRuleNode.class); + } + + private static void authorizeReferencingComponents( + final Authorizer authorizer, + final AuthorizableLookup lookup, + final String controllerServiceId, + final NiFiUser user, + final Class componentType) { + + final List referencingComponents = lookup.getControllerServiceReferencingComponents(controllerServiceId, componentType); + for (final Authorizable referencingComponent : referencingComponents) { + OperationAuthorizable.authorizeOperation(referencingComponent, authorizer, user); + } + } + + private AuthorizeControllerServiceReferencingComponents() { + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java index ce67307d4ad3..7e74bcf2bef1 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java @@ -531,6 +531,12 @@ public Authorizable getControllerServiceReferencingComponent(String controllerSe return reference; } + @Override + public List getControllerServiceReferencingComponents(final String controllerServiceId, final Class componentType) { + final ControllerServiceNode controllerService = controllerServiceDAO.getControllerService(controllerServiceId); + return List.copyOf(controllerService.getReferences().findRecursiveReferences(componentType)); + } + @Override public ComponentAuthorizable getReportingTask(final String id) { final ReportingTaskNode reportingTaskNode = reportingTaskDAO.getReportingTask(id); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java index 9b2d7988afe0..5d19e82c7971 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ControllerServiceResource.java @@ -43,6 +43,7 @@ import org.apache.nifi.authorization.AuthorizeComponentReference; import org.apache.nifi.authorization.AuthorizeConfigVerification; import org.apache.nifi.authorization.AuthorizeControllerServiceReference; +import org.apache.nifi.authorization.AuthorizeControllerServiceReferencingComponents; import org.apache.nifi.authorization.Authorizer; import org.apache.nifi.authorization.ComponentAuthorizable; import org.apache.nifi.authorization.RequestAction; @@ -569,6 +570,10 @@ public Response updateControllerServiceReferences( throw new IllegalArgumentException("The controller service identifier must be specified."); } + if (!id.equals(requestUpdateReferenceRequest.getId())) { + throw new IllegalArgumentException("The controller service identifier in the request must match the identifier provided in the URL"); + } + if (requestUpdateReferenceRequest.getReferencingComponentRevisions() == null) { throw new IllegalArgumentException("The controller service referencing components revisions must be specified."); } @@ -623,13 +628,9 @@ public Response updateControllerServiceReferences( serviceFacade, requestUpdateReferenceRequest, requestRevisions, - lookup -> { - requestReferencingRevisions.entrySet().stream().forEach(e -> { - final Authorizable controllerService = lookup.getControllerServiceReferencingComponent(id, e.getKey()); - OperationAuthorizable.authorizeOperation(controllerService, authorizer, NiFiUserUtils.getNiFiUser()); - }); - }, - () -> serviceFacade.verifyUpdateControllerServiceReferencingComponents(requestUpdateReferenceRequest.getId(), verifyScheduledState, verifyControllerServiceState), + lookup -> AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, id, verifyControllerServiceState, verifyScheduledState, NiFiUserUtils.getNiFiUser()), + () -> serviceFacade.verifyUpdateControllerServiceReferencingComponents(id, verifyScheduledState, verifyControllerServiceState), (revisions, updateReferenceRequest) -> { ScheduledState scheduledState = null; try { @@ -651,7 +652,7 @@ public Response updateControllerServiceReferences( // update the controller service references final ControllerServiceReferencingComponentsEntity entity = serviceFacade.updateControllerServiceReferencingComponents( - referencingRevisions, updateReferenceRequest.getId(), scheduledState, controllerServiceState); + referencingRevisions, id, scheduledState, controllerServiceState); if (updateReferenceRequest.getUiOnly() == Boolean.TRUE) { entity.getControllerServiceReferencingComponents().forEach(this::stripNonUiRelevantFields); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponentsTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponentsTest.java new file mode 100644 index 000000000000..5295c709321b --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeControllerServiceReferencingComponentsTest.java @@ -0,0 +1,189 @@ +/* + * 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.authorization; + +import org.apache.nifi.authorization.resource.Authorizable; +import org.apache.nifi.authorization.resource.ResourceFactory; +import org.apache.nifi.authorization.resource.ResourceType; +import org.apache.nifi.authorization.user.NiFiUser; +import org.apache.nifi.authorization.user.StandardNiFiUser; +import org.apache.nifi.controller.FlowAnalysisRuleNode; +import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.ReportingTaskNode; +import org.apache.nifi.controller.ScheduledState; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceState; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuthorizeControllerServiceReferencingComponentsTest { + + private static final String CONTROLLER_SERVICE_ID = "controller-service-id"; + + private static final String REFERENCING_SERVICE_ID = "referencing-service-id"; + + private final NiFiUser user = new StandardNiFiUser.Builder().identity("unit-test-user").build(); + + @Mock + private Authorizer authorizer; + + @Mock + private AuthorizableLookup lookup; + + @Mock + private ComponentAuthorizable controllerServiceAuthorizable; + + @Mock + private Authorizable controllerService; + + @Mock + private Authorizable referencingService; + + @Mock + private Authorizable referencingProcessor; + + @Mock + private Authorizable referencingReportingTask; + + @Mock + private Authorizable referencingFlowAnalysisRule; + + @Test + void testAuthorizeControllerServiceStateAuthorizesServiceAndReferencingServices() { + stubControllerService(); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ControllerServiceNode.class)) + .thenReturn(List.of(referencingService)); + + AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, ControllerServiceState.ENABLED, ScheduledState.DISABLED, user); + + verify(controllerService).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingService).authorize(authorizer, RequestAction.WRITE, user); + verify(lookup, never()).getControllerServiceReferencingComponents(eq(CONTROLLER_SERVICE_ID), eq(ProcessorNode.class)); + verify(lookup, never()).getControllerServiceReferencingComponents(eq(CONTROLLER_SERVICE_ID), eq(ReportingTaskNode.class)); + verify(lookup, never()).getControllerServiceReferencingComponents(eq(CONTROLLER_SERVICE_ID), eq(FlowAnalysisRuleNode.class)); + } + + @Test + void testAuthorizeScheduledStateAuthorizesServiceAndReferencingSchedulableComponents() { + stubControllerService(); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ProcessorNode.class)) + .thenReturn(List.of(referencingProcessor)); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ReportingTaskNode.class)) + .thenReturn(List.of(referencingReportingTask)); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, FlowAnalysisRuleNode.class)) + .thenReturn(List.of(referencingFlowAnalysisRule)); + + AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, null, ScheduledState.RUNNING, user); + + verify(controllerService).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingProcessor).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingReportingTask).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingFlowAnalysisRule).authorize(authorizer, RequestAction.WRITE, user); + verify(lookup, never()).getControllerServiceReferencingComponents(eq(CONTROLLER_SERVICE_ID), eq(ControllerServiceNode.class)); + } + + @Test + void testAuthorizeEveryReferencingComponentEvaluated() { + stubControllerService(); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ProcessorNode.class)) + .thenReturn(List.of(referencingProcessor, referencingFlowAnalysisRule)); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ReportingTaskNode.class)) + .thenReturn(List.of(referencingReportingTask)); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, FlowAnalysisRuleNode.class)) + .thenReturn(List.of()); + + AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, null, ScheduledState.STOPPED, user); + + verify(referencingProcessor).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingFlowAnalysisRule).authorize(authorizer, RequestAction.WRITE, user); + verify(referencingReportingTask).authorize(authorizer, RequestAction.WRITE, user); + } + + @Test + void testAuthorizeDeniedControllerService() { + stubControllerService(); + denyOperation(controllerService, CONTROLLER_SERVICE_ID); + + assertThrows(AccessDeniedException.class, () -> AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, ControllerServiceState.ENABLED, null, user)); + + verify(lookup, never()).getControllerServiceReferencingComponents(anyString(), any()); + } + + @Test + void testAuthorizeDeniedReferencingComponent() { + stubControllerService(); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ControllerServiceNode.class)) + .thenReturn(List.of(referencingService)); + denyOperation(referencingService, REFERENCING_SERVICE_ID); + + assertThrows(AccessDeniedException.class, () -> AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, ControllerServiceState.DISABLED, null, user)); + } + + @Test + void testAuthorizeWithoutRequestedState() { + stubControllerService(); + + AuthorizeControllerServiceReferencingComponents.authorize(authorizer, lookup, CONTROLLER_SERVICE_ID, null, null, user); + + verify(controllerService).authorize(authorizer, RequestAction.WRITE, user); + verify(lookup, never()).getControllerServiceReferencingComponents(anyString(), any()); + } + + @Test + void testAuthorizeWithoutReferencingComponents() { + stubControllerService(); + when(lookup.getControllerServiceReferencingComponents(CONTROLLER_SERVICE_ID, ControllerServiceNode.class)) + .thenReturn(List.of()); + + AuthorizeControllerServiceReferencingComponents.authorize( + authorizer, lookup, CONTROLLER_SERVICE_ID, ControllerServiceState.ENABLED, null, user); + + verify(controllerService).authorize(authorizer, RequestAction.WRITE, user); + } + + private void stubControllerService() { + when(lookup.getControllerService(CONTROLLER_SERVICE_ID)).thenReturn(controllerServiceAuthorizable); + when(controllerServiceAuthorizable.getAuthorizable()).thenReturn(controllerService); + } + + private void denyOperation(final Authorizable authorizable, final String identifier) { + doThrow(new AccessDeniedException("Access is denied")) + .when(authorizable).authorize(authorizer, RequestAction.WRITE, user); + when(authorizable.getResource()) + .thenReturn(ResourceFactory.getComponentResource(ResourceType.ControllerService, identifier, "Controller Service")); + when(authorizer.authorize(any())).thenReturn(AuthorizationResult.denied("Access is denied")); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/StandardAuthorizableLookupTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/StandardAuthorizableLookupTest.java index 1d8facc27453..a911a3e99f96 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/StandardAuthorizableLookupTest.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/StandardAuthorizableLookupTest.java @@ -30,6 +30,8 @@ import org.apache.nifi.controller.FlowAnalysisRuleNode; import org.apache.nifi.controller.ParameterProviderNode; import org.apache.nifi.controller.ProcessorNode; +import org.apache.nifi.controller.service.ControllerServiceNode; +import org.apache.nifi.controller.service.ControllerServiceReference; import org.apache.nifi.groups.ProcessGroup; import org.apache.nifi.nar.ExtensionDiscoveringManager; import org.apache.nifi.nar.ExtensionManager; @@ -39,6 +41,7 @@ import org.apache.nifi.web.dao.ConnectionDAO; import org.apache.nifi.web.dao.ConnectorDAO; import org.apache.nifi.web.dao.ConnectorManagedComponentLookup; +import org.apache.nifi.web.dao.ControllerServiceDAO; import org.apache.nifi.web.dao.FlowAnalysisRuleDAO; import org.apache.nifi.web.dao.FlowRegistryDAO; import org.apache.nifi.web.dao.ParameterProviderDAO; @@ -46,6 +49,7 @@ import org.apache.nifi.web.dao.ProcessorDAO; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -95,6 +99,25 @@ void testGetAuthorizableFromResource() { assertInstanceOf(ProcessorNode.class, ((OperationAuthorizable) authorizable).getBaseAuthorizable()); } + @Test + void testGetControllerServiceReferencingComponentsResolvesRecursiveReferencesForRequestedType() { + final StandardAuthorizableLookup lookup = getLookup(); + final ControllerServiceDAO controllerServiceDAO = mock(ControllerServiceDAO.class); + final ControllerServiceNode controllerService = mock(ControllerServiceNode.class); + final ControllerServiceReference references = mock(ControllerServiceReference.class); + final ControllerServiceNode referencingService = mock(ControllerServiceNode.class); + final ProcessorNode referencingProcessor = mock(ProcessorNode.class); + + when(controllerServiceDAO.getControllerService(eq(COMPONENT_ID))).thenReturn(controllerService); + when(controllerService.getReferences()).thenReturn(references); + when(references.findRecursiveReferences(eq(ControllerServiceNode.class))).thenReturn(List.of(referencingService)); + when(references.findRecursiveReferences(eq(ProcessorNode.class))).thenReturn(List.of(referencingProcessor)); + lookup.setControllerServiceDAO(controllerServiceDAO); + + assertEquals(List.of(referencingService), lookup.getControllerServiceReferencingComponents(COMPONENT_ID, ControllerServiceNode.class)); + assertEquals(List.of(referencingProcessor), lookup.getControllerServiceReferencingComponents(COMPONENT_ID, ProcessorNode.class)); + } + @Test void testGetAuthorizableFromResourceController() { final StandardAuthorizableLookup lookup = getLookup();