This is an automated email from the ASF dual-hosted git repository.

pvillard31 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 9cdecd3b7d4 NIFI-16263 Streamlined Process Group Update Authorization 
(#11601)
9cdecd3b7d4 is described below

commit 9cdecd3b7d48a805df4d22d725f9ac5a4c17e15d
Author: David Handermann <[email protected]>
AuthorDate: Mon Aug 31 04:35:06 2026 -0500

    NIFI-16263 Streamlined Process Group Update Authorization (#11601)
---
 .../nifi/authorization/AuthorizeFlowUpdate.java    | 130 +++++++++++++++
 .../nifi/authorization/AuthorizeProcessGroup.java  | 101 +++++++++++
 .../apache/nifi/web/api/ApplicationResource.java   |  86 +++-------
 .../apache/nifi/web/api/FlowUpdateResource.java    |  61 +------
 .../apache/nifi/web/api/ProcessGroupResource.java  |  13 +-
 .../org/apache/nifi/web/api/VersionsResource.java  |  40 ++---
 .../authorization/AuthorizeFlowUpdateTest.java     | 184 +++++++++++++++++++++
 7 files changed, 463 insertions(+), 152 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeFlowUpdate.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeFlowUpdate.java
new file mode 100644
index 00000000000..9200798633e
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeFlowUpdate.java
@@ -0,0 +1,130 @@
+/*
+ * 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.user.NiFiUser;
+import org.apache.nifi.flow.VersionedParameterContext;
+import org.apache.nifi.registry.flow.FlowSnapshotContainer;
+import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
+import org.apache.nifi.web.NiFiServiceFacade;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Authorizes replacing the contents of a Process Group with a proposed flow 
snapshot along with resolved references
+ */
+public final class AuthorizeFlowUpdate {
+    /**
+     * Identifiers of inherited Controller Services and Parameter Providers 
from a proposed snapshot that could not be
+     * resolved against the local flow.
+     *
+     * @param controllerServices unresolved inherited Controller Service 
identifiers
+     * @param parameterProviders unresolved Parameter Provider identifiers
+     */
+    public record UnresolvedReferences(Set<String> controllerServices, 
Set<String> parameterProviders) {
+    }
+
+    /**
+     * Resolves inherited references on the proposed snapshot and then 
authorizes the current user requesting flow updates
+     *
+     * @param groupId the id of the process group being updated
+     * @param flowSnapshot the proposed flow contents
+     * @param serviceFacade the service facade
+     * @param authorizer the authorizer
+     * @param lookup the authorizable lookup
+     * @param user the user to authorize
+     */
+    public static void resolveAndAuthorizeFlowUpdate(
+            final String groupId,
+            final RegisteredFlowSnapshot flowSnapshot,
+            final NiFiServiceFacade serviceFacade,
+            final Authorizer authorizer,
+            final AuthorizableLookup lookup,
+            final NiFiUser user
+    ) {
+        final FlowSnapshotContainer flowSnapshotContainer = new 
FlowSnapshotContainer(flowSnapshot);
+        final UnresolvedReferences unresolvedReferences = 
resolveReferences(groupId, flowSnapshotContainer, serviceFacade, user);
+        authorizeFlowUpdate(groupId, flowSnapshot, unresolvedReferences, 
serviceFacade, authorizer, lookup, user);
+    }
+
+    /**
+     * Discovers compatible bundles for the proposed snapshot and resolves the 
inherited Controller Services and the
+     * Parameter Providers that it references to components in the local flow
+     *
+     * @param groupId the id of the process group being updated
+     * @param flowSnapshotContainer the proposed flow snapshot along with the 
snapshots of any version controlled child groups
+     * @param serviceFacade the service facade
+     * @param user the user performing the update
+     * @return the references that could not be resolved against the local flow
+     */
+    public static UnresolvedReferences resolveReferences(
+            final String groupId,
+            final FlowSnapshotContainer flowSnapshotContainer,
+            final NiFiServiceFacade serviceFacade,
+            final NiFiUser user
+    ) {
+        final RegisteredFlowSnapshot flowSnapshot = 
flowSnapshotContainer.getFlowSnapshot();
+
+        // Discover compatible bundles since the flow snapshot can contain 
different versions
+        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getFlowContents());
+        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getParameterProviders());
+
+        // If there are any Controller Services referenced that are inherited 
from the parent group, resolve those to point to the appropriate Controller 
Service
+        final Set<String> unresolvedControllerServices = 
serviceFacade.resolveInheritedControllerServices(flowSnapshotContainer, 
groupId, user);
+
+        // If there are any Parameter Providers referenced by Parameter 
Contexts, resolve these to point to the appropriate Parameter Provider
+        final Set<String> unresolvedParameterProviders = 
serviceFacade.resolveParameterProviders(flowSnapshot, user);
+
+        return new UnresolvedReferences(unresolvedControllerServices, 
unresolvedParameterProviders);
+    }
+
+    /**
+     * Authorizes READ and WRITE permissions for the given user on the Process 
Group being updated
+     *
+     * @param groupId the id of the process group being updated
+     * @param flowSnapshot the proposed flow contents
+     * @param unresolvedReferences the references from the proposed snapshot 
that could not be resolved against the local flow
+     * @param serviceFacade the service facade
+     * @param authorizer the authorizer
+     * @param lookup the authorizable lookup
+     * @param user the user to authorize
+     */
+    public static void authorizeFlowUpdate(
+            final String groupId,
+            final RegisteredFlowSnapshot flowSnapshot,
+            final UnresolvedReferences unresolvedReferences,
+            final NiFiServiceFacade serviceFacade,
+            final Authorizer authorizer,
+            final AuthorizableLookup lookup,
+            final NiFiUser user
+    ) {
+        final ProcessGroupAuthorizable groupAuthorizable = 
lookup.getProcessGroup(groupId);
+        AuthorizeProcessGroup.authorizeProcessGroup(groupAuthorizable, 
authorizer, lookup, RequestAction.READ, true, false, true, false, true);
+        AuthorizeProcessGroup.authorizeProcessGroup(groupAuthorizable, 
authorizer, lookup, RequestAction.WRITE, true, false, true, false, false);
+
+        final Map<String, VersionedParameterContext> parameterContexts = 
flowSnapshot.getParameterContexts();
+        if (parameterContexts != null) {
+            for (final VersionedParameterContext parameterContext : 
parameterContexts.values()) {
+                
AuthorizeParameterReference.authorizeParameterContextAddition(parameterContext, 
serviceFacade, authorizer, lookup, user);
+            }
+        }
+
+        
AuthorizeParameterProviders.authorizeUnresolvedParameterProviders(unresolvedReferences.parameterProviders(),
 authorizer, lookup, user);
+        
AuthorizeControllerServiceReference.authorizeUnresolvedControllerServiceReferences(groupId,
 unresolvedReferences.controllerServices(), authorizer, lookup, user);
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeProcessGroup.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeProcessGroup.java
new file mode 100644
index 00000000000..d121b4ea51e
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeProcessGroup.java
@@ -0,0 +1,101 @@
+/*
+ * 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.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+
+import java.util.function.Consumer;
+
+/**
+ * Authorizes a Process Group along with encapsulated and referenced Components
+ */
+public final class AuthorizeProcessGroup {
+    /**
+     * Authorizes the specified Process Group and referenced Components
+     *
+     * @param processGroupAuthorizable process group
+     * @param authorizer authorizer
+     * @param lookup lookup
+     * @param action action
+     * @param authorizeReferencedServices whether to authorize referenced 
services
+     * @param authorizeControllerServices whether to authorize controller 
services
+     * @param authorizeTransitiveServices whether to authorize transitive 
services
+     * @param authorizeParameterReferences whether to authorize parameter 
context that contained referenced parameter if applicable
+     * @param authorizeParameterContext whether to authorize the bound 
parameter context if applicable
+     */
+    public static void authorizeProcessGroup(
+            final ProcessGroupAuthorizable processGroupAuthorizable,
+            final Authorizer authorizer,
+            final AuthorizableLookup lookup,
+            final RequestAction action,
+            final boolean authorizeReferencedServices,
+            final boolean authorizeControllerServices,
+            final boolean authorizeTransitiveServices,
+            final boolean authorizeParameterReferences,
+            final boolean authorizeParameterContext
+    ) {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final Consumer<Authorizable> authorize = authorizable -> 
authorizable.authorize(authorizer, action, user);
+
+
+        authorize.accept(processGroupAuthorizable.getAuthorizable());
+
+
+        if (authorizeParameterContext) {
+            
processGroupAuthorizable.getParameterContextAuthorizable().ifPresent(authorize);
+        }
+
+        
processGroupAuthorizable.getEncapsulatedProcessors().forEach(processorAuthorizable
 -> {
+            authorize.accept(processorAuthorizable.getAuthorizable());
+            if (authorizeReferencedServices) {
+                
AuthorizeControllerServiceReference.authorizeControllerServiceReferences(processorAuthorizable,
 authorizer, lookup, authorizeTransitiveServices);
+            }
+            if (authorizeParameterReferences) {
+                
AuthorizeParameterReference.authorizeParameterReferences(processorAuthorizable, 
authorizer, processorAuthorizable.getParameterContext(), user);
+            }
+        });
+        
processGroupAuthorizable.getEncapsulatedConnections().stream().map(AuthorizableHolder::getAuthorizable).forEach(authorize);
+        
processGroupAuthorizable.getEncapsulatedInputPorts().forEach(authorize);
+        
processGroupAuthorizable.getEncapsulatedOutputPorts().forEach(authorize);
+        processGroupAuthorizable.getEncapsulatedFunnels().forEach(authorize);
+        processGroupAuthorizable.getEncapsulatedLabels().forEach(authorize);
+        processGroupAuthorizable.getEncapsulatedProcessGroups().forEach(pga -> 
{
+            final Authorizable authorizable = pga.getAuthorizable();
+
+            authorize.accept(authorizable);
+
+            if (authorizeParameterContext) {
+                pga.getParameterContextAuthorizable().ifPresent(authorize);
+            }
+        });
+        
processGroupAuthorizable.getEncapsulatedRemoteProcessGroups().forEach(authorize);
+
+        if (authorizeControllerServices) {
+            
processGroupAuthorizable.getEncapsulatedControllerServices().forEach(controllerServiceAuthorizable
 -> {
+                
authorize.accept(controllerServiceAuthorizable.getAuthorizable());
+                if (authorizeReferencedServices) {
+                    
AuthorizeControllerServiceReference.authorizeControllerServiceReferences(controllerServiceAuthorizable,
 authorizer, lookup, authorizeTransitiveServices);
+                }
+                if (authorizeParameterReferences) {
+                    
AuthorizeParameterReference.authorizeParameterReferences(controllerServiceAuthorizable,
 authorizer, controllerServiceAuthorizable.getParameterContext(), user);
+                }
+            });
+        }
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index 81f16ad7057..b43c9614ef6 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -34,6 +34,7 @@ import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeAccess;
 import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
 import org.apache.nifi.authorization.AuthorizeParameterReference;
+import org.apache.nifi.authorization.AuthorizeProcessGroup;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ProcessGroupAuthorizable;
 import org.apache.nifi.authorization.RequestAction;
@@ -430,69 +431,28 @@ public abstract class ApplicationResource {
      * @param authorizeParameterReferences whether to authorize parameter 
context that contained referenced parameter if applicable
      * @param authorizeParameterContext whether to authorize the bound 
parameter context if applicable
      */
-    protected void authorizeProcessGroup(final ProcessGroupAuthorizable 
processGroupAuthorizable, final Authorizer authorizer, final AuthorizableLookup 
lookup, final RequestAction action,
-                                         final boolean 
authorizeReferencedServices,
-                                         final boolean 
authorizeControllerServices, final boolean authorizeTransitiveServices,
-                                         final boolean 
authorizeParameterReferences, final boolean authorizeParameterContext) {
-
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        final Consumer<Authorizable> authorize = authorizable -> 
authorizable.authorize(authorizer, action, user);
-
-        // authorize the process group
-        authorize.accept(processGroupAuthorizable.getAuthorizable());
-
-        // authorize the parameter context for the specified process group
-        if (authorizeParameterContext) {
-            
processGroupAuthorizable.getParameterContextAuthorizable().ifPresent(authorize);
-        }
-
-        // authorize the contents of the group - these methods return all 
encapsulated components (recursive)
-        
processGroupAuthorizable.getEncapsulatedProcessors().forEach(processorAuthorizable
 -> {
-            // authorize the processor
-            authorize.accept(processorAuthorizable.getAuthorizable());
-
-            // authorize any referenced services if necessary
-            if (authorizeReferencedServices) {
-                
AuthorizeControllerServiceReference.authorizeControllerServiceReferences(processorAuthorizable,
 authorizer, lookup, authorizeTransitiveServices);
-            }
-
-            // authorize any referenced parameters if necessary
-            if (authorizeParameterReferences) {
-                
AuthorizeParameterReference.authorizeParameterReferences(processorAuthorizable, 
authorizer, processorAuthorizable.getParameterContext(), user);
-            }
-        });
-        
processGroupAuthorizable.getEncapsulatedConnections().stream().map(connection 
-> connection.getAuthorizable()).forEach(authorize);
-        
processGroupAuthorizable.getEncapsulatedInputPorts().forEach(authorize);
-        
processGroupAuthorizable.getEncapsulatedOutputPorts().forEach(authorize);
-        processGroupAuthorizable.getEncapsulatedFunnels().forEach(authorize);
-        processGroupAuthorizable.getEncapsulatedLabels().forEach(authorize);
-        processGroupAuthorizable.getEncapsulatedProcessGroups().forEach(pga -> 
{
-            final Authorizable authorizable = pga.getAuthorizable();
-
-            authorize.accept(authorizable);
-
-            if (authorizeParameterContext) {
-                pga.getParameterContextAuthorizable().ifPresent(authorize);
-            }
-        });
-        
processGroupAuthorizable.getEncapsulatedRemoteProcessGroups().forEach(authorize);
-
-        // authorize controller services if necessary
-        if (authorizeControllerServices) {
-            
processGroupAuthorizable.getEncapsulatedControllerServices().forEach(controllerServiceAuthorizable
 -> {
-                // authorize the controller service
-                
authorize.accept(controllerServiceAuthorizable.getAuthorizable());
-
-                // authorize any referenced services if necessary
-                if (authorizeReferencedServices) {
-                    
AuthorizeControllerServiceReference.authorizeControllerServiceReferences(controllerServiceAuthorizable,
 authorizer, lookup, authorizeTransitiveServices);
-                }
-
-                if (authorizeParameterReferences) {
-                    
AuthorizeParameterReference.authorizeParameterReferences(controllerServiceAuthorizable,
 authorizer, controllerServiceAuthorizable.getParameterContext(), user);
-                }
-            });
-        }
+    protected void authorizeProcessGroup(
+            final ProcessGroupAuthorizable processGroupAuthorizable,
+            final Authorizer authorizer,
+            final AuthorizableLookup lookup,
+            final RequestAction action,
+            final boolean authorizeReferencedServices,
+            final boolean authorizeControllerServices,
+            final boolean authorizeTransitiveServices,
+            final boolean authorizeParameterReferences,
+            final boolean authorizeParameterContext
+    ) {
+        AuthorizeProcessGroup.authorizeProcessGroup(
+                processGroupAuthorizable,
+                authorizer,
+                lookup,
+                action,
+                authorizeReferencedServices,
+                authorizeControllerServices,
+                authorizeTransitiveServices,
+                authorizeParameterReferences,
+                authorizeParameterContext
+        );
     }
 
     /**
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowUpdateResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowUpdateResource.java
index 143b784605d..b999b1f60ba 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowUpdateResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowUpdateResource.java
@@ -20,19 +20,14 @@ import jakarta.ws.rs.HttpMethod;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.Status;
-import org.apache.nifi.authorization.AuthorizableLookup;
-import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
-import org.apache.nifi.authorization.AuthorizeParameterProviders;
-import org.apache.nifi.authorization.AuthorizeParameterReference;
+import org.apache.nifi.authorization.AuthorizeFlowUpdate;
+import org.apache.nifi.authorization.AuthorizeFlowUpdate.UnresolvedReferences;
 import org.apache.nifi.authorization.Authorizer;
-import org.apache.nifi.authorization.ProcessGroupAuthorizable;
-import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
 import org.apache.nifi.authorization.user.NiFiUserUtils;
 import org.apache.nifi.cluster.manager.NodeResponse;
 import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.service.ControllerServiceState;
-import org.apache.nifi.flow.VersionedParameterContext;
 import org.apache.nifi.registry.flow.FlowSnapshotContainer;
 import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
 import org.apache.nifi.web.NiFiServiceFacade;
@@ -195,17 +190,7 @@ public abstract class FlowUpdateResource<T extends 
ProcessGroupDescriptorEntity,
         // Step 0: Obtain the versioned flow snapshot to use for the update
         final FlowSnapshotContainer flowSnapshotContainer = 
flowSnapshotContainerSupplier.get();
         final RegisteredFlowSnapshot flowSnapshot = 
flowSnapshotContainer.getFlowSnapshot();
-
-        // The new flow may not contain the same versions of components in 
existing flow. As a result, we need to update
-        // the flow snapshot to contain compatible bundles.
-        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getFlowContents());
-        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getParameterProviders());
-
-        // If there are any Controller Services referenced that are inherited 
from the parent group, resolve those to point to the appropriate Controller 
Service, if we are able to.
-        final Set<String> unresolvedControllerServices = 
serviceFacade.resolveInheritedControllerServices(flowSnapshotContainer, 
groupId, user);
-
-        // If there are any Parameter Providers referenced by Parameter 
Contexts, resolve these to point to the appropriate Parameter Provider, if we 
are able to.
-        final Set<String> unresolvedParameterProviders = 
serviceFacade.resolveParameterProviders(flowSnapshot, user);
+        final UnresolvedReferences unresolvedReferences = 
AuthorizeFlowUpdate.resolveReferences(groupId, flowSnapshotContainer, 
serviceFacade, user);
 
         // Step 1: Determine which components will be affected by updating the 
flow
         final Set<AffectedComponentEntity> affectedComponents = 
serviceFacade.getComponentsAffectedByFlowUpdate(groupId, flowSnapshot);
@@ -220,7 +205,7 @@ public abstract class FlowUpdateResource<T extends 
ProcessGroupDescriptorEntity,
                 serviceFacade,
                 requestWrapper,
                 requestRevision,
-                lookup -> authorizeFlowUpdate(lookup, user, groupId, 
flowSnapshot, unresolvedControllerServices, unresolvedParameterProviders),
+                lookup -> AuthorizeFlowUpdate.authorizeFlowUpdate(groupId, 
flowSnapshot, unresolvedReferences, serviceFacade, authorizer, lookup, user),
                 () -> {
                     // Step 3: Verify that all components in the snapshot 
exist on all nodes
                     // Step 4: Verify that Process Group can be updated. Only 
versioned flows care about the verifyNotDirty flag
@@ -230,38 +215,6 @@ public abstract class FlowUpdateResource<T extends 
ProcessGroupDescriptorEntity,
         );
     }
 
-    /**
-     * Authorize read/write permissions for the given user on every component 
of the given flow in support of flow update.
-     *
-     * @param lookup A lookup instance to use for retrieving components for 
authorization purposes
-     * @param user the user to authorize
-     * @param groupId the id of the process group being evaluated
-     * @param flowSnapshot the new flow contents to authorize
-     */
-    protected void authorizeFlowUpdate(final AuthorizableLookup lookup, final 
NiFiUser user, final String groupId,
-                                       final RegisteredFlowSnapshot 
flowSnapshot, final Set<String> unresolvedControllerServices,
-                                       final Set<String> 
unresolvedParameterProviders) {
-        // Step 2: Verify READ and WRITE permissions for user, for every 
component.
-        final ProcessGroupAuthorizable groupAuthorizable = 
lookup.getProcessGroup(groupId);
-        authorizeProcessGroup(groupAuthorizable, authorizer, lookup, 
RequestAction.READ, true,
-                false, true, false, true);
-        authorizeProcessGroup(groupAuthorizable, authorizer, lookup, 
RequestAction.WRITE, true,
-                false, true, false, false);
-
-        final Map<String, VersionedParameterContext> parameterContexts = 
flowSnapshot.getParameterContexts();
-        if (parameterContexts != null) {
-            parameterContexts.values().forEach(
-                    context -> 
AuthorizeParameterReference.authorizeParameterContextAddition(context, 
serviceFacade, authorizer, lookup, user)
-            );
-        }
-
-        // authorize parameter providers
-        
AuthorizeParameterProviders.authorizeUnresolvedParameterProviders(unresolvedParameterProviders,
 authorizer, lookup, user);
-
-        // authorizer controller services
-        
AuthorizeControllerServiceReference.authorizeUnresolvedControllerServiceReferences(groupId,
 unresolvedControllerServices, authorizer, lookup, user);
-    }
-
     /**
      * Create and submit the flow update request. Return response containing 
an entity reflecting the status of the async request.
      * <p>
@@ -401,11 +354,7 @@ public abstract class FlowUpdateResource<T extends 
ProcessGroupDescriptorEntity,
 
             // Resolve compatible bundles, inherited controller services, and 
parameter providers for the rollback snapshot before any
             // replication occurs, ensuring that all nodes in the cluster 
receive the same resolved references.
-            
serviceFacade.discoverCompatibleBundles(originalFlowSnapshot.getFlowContents());
-            
serviceFacade.discoverCompatibleBundles(originalFlowSnapshot.getParameterProviders());
-            final NiFiUser user = NiFiUserUtils.getNiFiUser();
-            
serviceFacade.resolveInheritedControllerServices(originalFlowSnapshotContainer, 
groupId, user);
-            serviceFacade.resolveParameterProviders(originalFlowSnapshot, 
user);
+            AuthorizeFlowUpdate.resolveReferences(groupId, 
originalFlowSnapshotContainer, serviceFacade, NiFiUserUtils.getNiFiUser());
         }
 
         try {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
index 77c43f836d3..6074142e03a 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProcessGroupResource.java
@@ -48,6 +48,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AuthorizableLookup;
 import org.apache.nifi.authorization.AuthorizeComponentReference;
 import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
+import org.apache.nifi.authorization.AuthorizeFlowUpdate;
 import org.apache.nifi.authorization.AuthorizeParameterProviders;
 import org.apache.nifi.authorization.AuthorizeParameterReference;
 import org.apache.nifi.authorization.ConnectionAuthorizable;
@@ -3275,7 +3276,10 @@ public class ProcessGroupResource extends 
FlowUpdateResource<ProcessGroupImportE
                     + NON_GUARANTEED_ENDPOINT,
             security = {
                     @SecurityRequirement(name = "Read - 
/process-groups/{uuid}"),
-                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}")
+                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Read - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Write - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Read - 
/parameter-contexts/{uuid} - For any Parameter Context that is referenced by a 
Property that is changed, added, or removed")
             }
     )
     public Response replaceProcessGroup(
@@ -3316,12 +3320,7 @@ public class ProcessGroupResource extends 
FlowUpdateResource<ProcessGroupImportE
                 serviceFacade,
                 importEntity,
                 requestRevision,
-                lookup -> {
-                    final ProcessGroupAuthorizable groupAuthorizable = 
lookup.getProcessGroup(groupId);
-                    final Authorizable processGroup = 
groupAuthorizable.getAuthorizable();
-                    processGroup.authorize(authorizer, RequestAction.READ, 
NiFiUserUtils.getNiFiUser());
-                    processGroup.authorize(authorizer, RequestAction.WRITE, 
NiFiUserUtils.getNiFiUser());
-                },
+                lookup -> 
AuthorizeFlowUpdate.resolveAndAuthorizeFlowUpdate(groupId, requestFlowSnapshot, 
serviceFacade, authorizer, lookup, NiFiUserUtils.getNiFiUser()),
                 () -> {
                     // We do not enforce that the Process Group is 'not dirty' 
because at this point,
                     // the client has explicitly indicated the dataflow that 
the Process Group should
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
index 6bbaf98b676..00a1133109b 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/VersionsResource.java
@@ -42,6 +42,8 @@ import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.Status;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.authorization.AuthorizeFlowUpdate;
+import org.apache.nifi.authorization.AuthorizeFlowUpdate.UnresolvedReferences;
 import org.apache.nifi.authorization.ProcessGroupAuthorizable;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.resource.Authorizable;
@@ -877,7 +879,10 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
                     + NON_GUARANTEED_ENDPOINT,
             security = {
                     @SecurityRequirement(name = "Read - 
/process-groups/{uuid}"),
-                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}")
+                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Read - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Write - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Read - 
/parameter-contexts/{uuid} - For any Parameter Context that is referenced by a 
Property that is changed, added, or removed")
             }
     )
     public Response updateFlowVersion(
@@ -922,12 +927,7 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
                 serviceFacade,
                 requestEntity,
                 requestRevision,
-                lookup -> {
-                    final ProcessGroupAuthorizable groupAuthorizable = 
lookup.getProcessGroup(groupId);
-                    final Authorizable processGroup = 
groupAuthorizable.getAuthorizable();
-                    processGroup.authorize(authorizer, RequestAction.READ, 
NiFiUserUtils.getNiFiUser());
-                    processGroup.authorize(authorizer, RequestAction.WRITE, 
NiFiUserUtils.getNiFiUser());
-                },
+                lookup -> 
AuthorizeFlowUpdate.resolveAndAuthorizeFlowUpdate(groupId, requestFlowSnapshot, 
serviceFacade, authorizer, lookup, NiFiUserUtils.getNiFiUser()),
                 () -> {
                     // We do not enforce that the Process Group is 'not dirty' 
because at this point,
                     // the client has explicitly indicated the dataflow that 
the Process Group should
@@ -978,7 +978,10 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
                     + NON_GUARANTEED_ENDPOINT,
             security = {
                     @SecurityRequirement(name = "Read - 
/process-groups/{uuid}"),
-                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}")
+                    @SecurityRequirement(name = "Write - 
/process-groups/{uuid}"),
+                    @SecurityRequirement(name = "Read - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Write - 
/{component-type}/{uuid} - For all encapsulated components"),
+                    @SecurityRequirement(name = "Read - 
/parameter-contexts/{uuid} - For any Parameter Context that is referenced by a 
Property that is changed, added, or removed")
             }
     )
     public Response applyRebasedFlowVersion(
@@ -1023,12 +1026,7 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
                 serviceFacade,
                 requestEntity,
                 requestRevision,
-                lookup -> {
-                    final ProcessGroupAuthorizable groupAuthorizable = 
lookup.getProcessGroup(groupId);
-                    final Authorizable processGroup = 
groupAuthorizable.getAuthorizable();
-                    processGroup.authorize(authorizer, RequestAction.READ, 
NiFiUserUtils.getNiFiUser());
-                    processGroup.authorize(authorizer, RequestAction.WRITE, 
NiFiUserUtils.getNiFiUser());
-                },
+                lookup -> 
AuthorizeFlowUpdate.resolveAndAuthorizeFlowUpdate(groupId, requestFlowSnapshot, 
serviceFacade, authorizer, lookup, NiFiUserUtils.getNiFiUser()),
                 () -> {
                     // We do not enforce that the Process Group is 'not dirty' 
because a rebase intentionally applies
                     // over locally modified flows.
@@ -1510,17 +1508,7 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
         // Step 0: Get the Versioned Flow Snapshot from the Flow Registry
         final FlowSnapshotContainer flowSnapshotContainer = 
serviceFacade.getVersionedFlowSnapshot(requestEntity.getVersionControlInformation(),
 true);
         final RegisteredFlowSnapshot flowSnapshot = 
flowSnapshotContainer.getFlowSnapshot();
-
-        // The flow in the registry may not contain the same versions of 
components that we have in our flow. As a result, we need to update
-        // the flow snapshot to contain compatible bundles.
-        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getFlowContents());
-        
serviceFacade.discoverCompatibleBundles(flowSnapshot.getParameterProviders());
-
-        // If there are any Controller Services referenced that are inherited 
from the parent group, resolve those to point to the appropriate Controller 
Service, if we are able to.
-        final Set<String> unresolvedControllerServices = 
serviceFacade.resolveInheritedControllerServices(flowSnapshotContainer, 
groupId, NiFiUserUtils.getNiFiUser());
-
-        // If there are any Parameter Providers referenced by Parameter 
Contexts, resolve these to point to the appropriate Parameter Provider, if we 
are able to.
-        final Set<String> unresolvedParameterProviders = 
serviceFacade.resolveParameterProviders(flowSnapshot, 
NiFiUserUtils.getNiFiUser());
+        final UnresolvedReferences unresolvedReferences = 
AuthorizeFlowUpdate.resolveReferences(groupId, flowSnapshotContainer, 
serviceFacade, user);
 
         // Step 1: Determine which components will be affected by updating the 
version
         final Set<AffectedComponentEntity> affectedComponents = 
serviceFacade.getComponentsAffectedByFlowUpdate(groupId, flowSnapshot);
@@ -1535,7 +1523,7 @@ public class VersionsResource extends 
FlowUpdateResource<VersionControlInformati
                 serviceFacade,
                 requestWrapper,
                 requestRevision,
-                lookup -> authorizeFlowUpdate(lookup, user, groupId, 
flowSnapshot, unresolvedControllerServices, unresolvedParameterProviders),
+                lookup -> AuthorizeFlowUpdate.authorizeFlowUpdate(groupId, 
flowSnapshot, unresolvedReferences, serviceFacade, authorizer, lookup, user),
                 () -> {
                     // Step 3: Verify that all components in the snapshot 
exist on all nodes
                     // Step 4: Verify that Process Group is already under 
version control. If not, must start Version Control instead of updating flow
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeFlowUpdateTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeFlowUpdateTest.java
new file mode 100644
index 00000000000..402292085a0
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeFlowUpdateTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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.AuthorizeFlowUpdate.UnresolvedReferences;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.flow.VersionedParameter;
+import org.apache.nifi.flow.VersionedParameterContext;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.FlowSnapshotContainer;
+import org.apache.nifi.registry.flow.RegisteredFlowSnapshot;
+import org.apache.nifi.web.NiFiServiceFacade;
+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.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class AuthorizeFlowUpdateTest {
+
+    private static final String GROUP_ID = "group-id";
+
+    private static final String PARAMETER_CONTEXT_NAME = "Parameter Context";
+
+    private static final String PARAMETER_PROVIDER_ID = 
"parameter-provider-id";
+
+    private static final String CONTROLLER_SERVICE_ID = 
"controller-service-id";
+
+    @Mock
+    private Authorizer authorizer;
+
+    @Mock
+    private AuthorizableLookup lookup;
+
+    @Mock
+    private NiFiServiceFacade serviceFacade;
+
+    @Mock
+    private NiFiUser user;
+
+    @Mock
+    private ProcessGroupAuthorizable groupAuthorizable;
+
+    @Mock
+    private Authorizable groupResource;
+
+    @Mock
+    private ComponentAuthorizable encapsulatedProcessor;
+
+    @Mock
+    private Authorizable encapsulatedProcessorResource;
+
+    @Mock
+    private Authorizable parameterContexts;
+
+    @Mock
+    private Authorizable controller;
+
+    @Test
+    void 
testResolveAndAuthorizeFlowUpdateResolvesReferencesBeforeAuthorizing() {
+        final RegisteredFlowSnapshot flowSnapshot = createFlowSnapshot();
+        
when(lookup.getProcessGroup(eq(GROUP_ID))).thenReturn(groupAuthorizable);
+        when(groupAuthorizable.getAuthorizable()).thenReturn(groupResource);
+
+        AuthorizeFlowUpdate.resolveAndAuthorizeFlowUpdate(GROUP_ID, 
flowSnapshot, serviceFacade, authorizer, lookup, user);
+
+        
verify(serviceFacade).discoverCompatibleBundles(eq(flowSnapshot.getFlowContents()));
+        
verify(serviceFacade).discoverCompatibleBundles(eq(flowSnapshot.getParameterProviders()));
+        
verify(serviceFacade).resolveInheritedControllerServices(any(FlowSnapshotContainer.class),
 eq(GROUP_ID), eq(user));
+        verify(serviceFacade).resolveParameterProviders(eq(flowSnapshot), 
eq(user));
+
+        verify(groupResource).authorize(eq(authorizer), 
eq(RequestAction.READ), any());
+        verify(groupResource).authorize(eq(authorizer), 
eq(RequestAction.WRITE), any());
+    }
+
+    @Test
+    void testResolveAndAuthorizeFlowUpdateDeniedEncapsulatedProcessor() {
+        final RegisteredFlowSnapshot flowSnapshot = createFlowSnapshot();
+        
when(lookup.getProcessGroup(eq(GROUP_ID))).thenReturn(groupAuthorizable);
+        when(groupAuthorizable.getAuthorizable()).thenReturn(groupResource);
+        
when(groupAuthorizable.getEncapsulatedProcessors()).thenReturn(Set.of(encapsulatedProcessor));
+        
when(encapsulatedProcessor.getAuthorizable()).thenReturn(encapsulatedProcessorResource);
+        
doNothing().when(encapsulatedProcessorResource).authorize(eq(authorizer), 
eq(RequestAction.READ), any());
+        doThrow(new 
AccessDeniedException("denied")).when(encapsulatedProcessorResource).authorize(eq(authorizer),
 eq(RequestAction.WRITE), any());
+
+        assertThrows(AccessDeniedException.class,
+                () -> 
AuthorizeFlowUpdate.resolveAndAuthorizeFlowUpdate(GROUP_ID, flowSnapshot, 
serviceFacade, authorizer, lookup, user));
+    }
+
+    @Test
+    void testAuthorizeFlowUpdateDeniedParameterContextCreation() {
+        final RegisteredFlowSnapshot flowSnapshot = createFlowSnapshot();
+        flowSnapshot.setParameterContexts(Map.of(PARAMETER_CONTEXT_NAME, 
createParameterContext()));
+
+        
when(lookup.getProcessGroup(eq(GROUP_ID))).thenReturn(groupAuthorizable);
+        when(groupAuthorizable.getAuthorizable()).thenReturn(groupResource);
+        
when(serviceFacade.getParameterContextByName(eq(PARAMETER_CONTEXT_NAME), 
eq(user))).thenReturn(null);
+        when(lookup.getParameterContexts()).thenReturn(parameterContexts);
+        doThrow(new 
AccessDeniedException("denied")).when(parameterContexts).authorize(eq(authorizer),
 eq(RequestAction.WRITE), eq(user));
+
+        final UnresolvedReferences unresolvedReferences = new 
UnresolvedReferences(Set.of(), Set.of());
+
+        assertThrows(AccessDeniedException.class,
+                () -> AuthorizeFlowUpdate.authorizeFlowUpdate(GROUP_ID, 
flowSnapshot, unresolvedReferences, serviceFacade, authorizer, lookup, user));
+    }
+
+    @Test
+    void testAuthorizeFlowUpdateDeniedUnresolvedParameterProvider() {
+        final RegisteredFlowSnapshot flowSnapshot = createFlowSnapshot();
+        
when(lookup.getProcessGroup(eq(GROUP_ID))).thenReturn(groupAuthorizable);
+        when(groupAuthorizable.getAuthorizable()).thenReturn(groupResource);
+        when(lookup.getController()).thenReturn(controller);
+        doThrow(new 
AccessDeniedException("denied")).when(controller).authorize(eq(authorizer), 
eq(RequestAction.WRITE), eq(user));
+
+        final UnresolvedReferences unresolvedReferences = new 
UnresolvedReferences(Set.of(), Set.of(PARAMETER_PROVIDER_ID));
+
+        assertThrows(AccessDeniedException.class,
+                () -> AuthorizeFlowUpdate.authorizeFlowUpdate(GROUP_ID, 
flowSnapshot, unresolvedReferences, serviceFacade, authorizer, lookup, user));
+    }
+
+    @Test
+    void testResolveReferencesReturnsUnresolvedIdentifiers() {
+        final RegisteredFlowSnapshot flowSnapshot = createFlowSnapshot();
+        final FlowSnapshotContainer flowSnapshotContainer = new 
FlowSnapshotContainer(flowSnapshot);
+        
when(serviceFacade.resolveInheritedControllerServices(eq(flowSnapshotContainer),
 eq(GROUP_ID), eq(user))).thenReturn(Set.of(CONTROLLER_SERVICE_ID));
+        when(serviceFacade.resolveParameterProviders(eq(flowSnapshot), 
eq(user))).thenReturn(Set.of(PARAMETER_PROVIDER_ID));
+
+        final UnresolvedReferences unresolvedReferences = 
AuthorizeFlowUpdate.resolveReferences(GROUP_ID, flowSnapshotContainer, 
serviceFacade, user);
+
+        assertEquals(Set.of(CONTROLLER_SERVICE_ID), 
unresolvedReferences.controllerServices());
+        assertEquals(Set.of(PARAMETER_PROVIDER_ID), 
unresolvedReferences.parameterProviders());
+    }
+
+    private RegisteredFlowSnapshot createFlowSnapshot() {
+        final VersionedProcessGroup flowContents = new VersionedProcessGroup();
+        flowContents.setIdentifier(GROUP_ID);
+
+        final RegisteredFlowSnapshot flowSnapshot = new 
RegisteredFlowSnapshot();
+        flowSnapshot.setFlowContents(flowContents);
+        flowSnapshot.setParameterContexts(Map.of());
+        flowSnapshot.setParameterProviders(Map.of());
+
+        return flowSnapshot;
+    }
+
+    private VersionedParameterContext createParameterContext() {
+        final VersionedParameter parameter = new VersionedParameter();
+        parameter.setName("parameter");
+        parameter.setValue("value");
+
+        final VersionedParameterContext parameterContext = new 
VersionedParameterContext();
+        parameterContext.setName(PARAMETER_CONTEXT_NAME);
+        parameterContext.setParameters(Set.of(parameter));
+
+        return parameterContext;
+    }
+}

Reply via email to