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 38041ab7d19 NIFI-16419 Standardized Authorization for Parameter 
Context Creation (#11775)
38041ab7d19 is described below

commit 38041ab7d1973eccdc702ed0f9cf8201660aa842
Author: David Handermann <[email protected]>
AuthorDate: Wed Oct 7 05:24:13 2026 -0500

    NIFI-16419 Standardized Authorization for Parameter Context Creation 
(#11775)
    
    - Added AuthorizeParameterProviderApply for handling conditional Parameter 
Context authorization based on required operations
---
 .../AuthorizeParameterProviderApply.java           |  69 ++++++++++++
 .../nifi/web/api/ParameterProviderResource.java    | 108 +++++++++++--------
 .../AuthorizeParameterProviderApplyTest.java       | 118 +++++++++++++++++++++
 3 files changed, 252 insertions(+), 43 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeParameterProviderApply.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeParameterProviderApply.java
new file mode 100644
index 00000000000..62198fac596
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizeParameterProviderApply.java
@@ -0,0 +1,69 @@
+/*
+ * 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.web.api.entity.ParameterContextEntity;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Authorization helpers for applying Parameter Provider fetched parameters to 
Parameter Contexts.
+ */
+public final class AuthorizeParameterProviderApply {
+
+    private AuthorizeParameterProviderApply() {
+    }
+
+    /**
+     * Authorizes a Parameter Provider apply-parameters request. Requires READ 
on the Parameter Provider. When the request
+     * will create one or more Parameter Contexts, requires WRITE on the 
Parameter Contexts resource. Requires READ and
+     * WRITE on each existing Parameter Context that will be updated.
+     *
+     * @param parameterProviderId the Parameter Provider identifier
+     * @param requiresParameterContextCreation whether the request will create 
one or more Parameter Contexts
+     * @param parameterContextUpdates existing Parameter Contexts that will be 
updated
+     * @param authorizer the Authorizer
+     * @param lookup the AuthorizableLookup
+     * @param user the current user
+     */
+    public static void authorizeApplyParameters(
+            final String parameterProviderId,
+            final boolean requiresParameterContextCreation,
+            final Collection<ParameterContextEntity> parameterContextUpdates,
+            final Authorizer authorizer,
+            final AuthorizableLookup lookup,
+            final NiFiUser user
+    ) {
+        final ComponentAuthorizable parameterProvider = 
lookup.getParameterProvider(parameterProviderId);
+        parameterProvider.getAuthorizable().authorize(authorizer, 
RequestAction.READ, user);
+
+        if (requiresParameterContextCreation) {
+            // Creating a Parameter Context requires the same global WRITE 
permission as POST /parameter-contexts.
+            lookup.getParameterContexts().authorize(authorizer, 
RequestAction.WRITE, user);
+        }
+
+        final Collection<ParameterContextEntity> updates = 
parameterContextUpdates == null ? List.of() : parameterContextUpdates;
+        for (final ParameterContextEntity context : updates) {
+            final Authorizable parameterContext = 
lookup.getParameterContext(context.getComponent().getId());
+            parameterContext.authorize(authorizer, RequestAction.READ, user);
+            parameterContext.authorize(authorizer, RequestAction.WRITE, user);
+        }
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
index 1f3a5a4ff04..588826ae289 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ParameterProviderResource.java
@@ -46,6 +46,7 @@ import 
org.apache.nifi.authorization.AuthorizeComponentAnalysis;
 import org.apache.nifi.authorization.AuthorizeComponentReference;
 import org.apache.nifi.authorization.AuthorizeConfigVerification;
 import org.apache.nifi.authorization.AuthorizeControllerServiceReference;
+import org.apache.nifi.authorization.AuthorizeParameterProviderApply;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.ComponentAuthorizable;
 import org.apache.nifi.authorization.RequestAction;
@@ -915,6 +916,9 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
             security = {
                     @SecurityRequirement(name = "Read - 
/parameter-providers/{parameterProviderId}"),
                     @SecurityRequirement(name = "Write - 
/parameter-providers/{parameterProviderId}"),
+                    @SecurityRequirement(name = "Write - /parameter-contexts - 
when applying creates a new Parameter Context"),
+                    @SecurityRequirement(name = "Read - for every Parameter 
Context that is affected by the update"),
+                    @SecurityRequirement(name = "Write - for every Parameter 
Context that is affected by the update"),
                     @SecurityRequirement(name = "Read - for every component 
that is affected by the update"),
                     @SecurityRequirement(name = "Write - for every component 
that is affected by the update")
             }
@@ -952,35 +956,30 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
         // 1. Determine which components will be affected and are 
enabled/running
         // 2. Verify READ and WRITE permissions for user, for every component 
that is affected
         // 3. Verify READ and WRITE permissions for user, for each referenced 
Parameter Context
-        // 4. Stop all Processors that are affected.
-        // 5. Wait for all of the Processors to finish stopping.
-        // 6. Disable all Controller Services that are affected.
-        // 7. Wait for all Controller Services to finish disabling.
-        // 8. Update Parameter Contexts with fetched parameters from the 
Parameter Provider
-        // 9. Re-Enable all affected Controller Services
-        // 10. Re-Start all Processors
+        // 4. Verify WRITE permission for Parameter Contexts when a new 
Parameter Context must be created
+        // 5. Create any required Parameter Contexts
+        // 6. Stop all Processors that are affected.
+        // 7. Wait for all of the Processors to finish stopping.
+        // 8. Disable all Controller Services that are affected.
+        // 9. Wait for all Controller Services to finish disabling.
+        // 10. Update Parameter Contexts with fetched parameters from the 
Parameter Provider
+        // 11. Re-Enable all affected Controller Services
+        // 12. Re-Start all Processors
 
         final Collection<ParameterGroupConfiguration> 
parameterGroupConfigurations = 
getParameterGroupConfigurations(requestEntity.getParameterGroupConfigurations());
         validateParameterGroupConfigurations(parameterProviderId, 
parameterGroupConfigurations, user);
-        parameterGroupConfigurations.stream()
-                .filter(parameterGroupConfiguration -> 
requiresNewParameterContext(parameterGroupConfiguration, user))
-                .forEach(parameterGroupConfiguration -> {
-                    final ParameterContextEntity newParameterContext = 
getNewParameterContextEntity(parameterProviderId, parameterGroupConfiguration);
-                    try {
-                        performParameterContextCreate(user, getAbsolutePath(), 
replicateRequest, newParameterContext);
-                    } catch (final LifecycleManagementException e) {
-                        throw new RuntimeException("Failed to create Parameter 
Context " + parameterGroupConfiguration.getGroupName(), e);
-                    }
-                });
 
-        // Get a list of parameter context entities representing changes 
needed in order to apply the fetched parameters
+        final boolean requiresParameterContextCreation = 
parameterGroupConfigurations.stream()
+                .anyMatch(parameterGroupConfiguration -> 
requiresNewParameterContext(parameterGroupConfiguration, user));
+
+        // Preliminary updates for Parameter Contexts that already exist. New 
Parameter Contexts are created only after authorization.
         final List<ParameterContextEntity> parameterContextUpdates = 
serviceFacade.getParameterContextUpdatesForAppliedParameters(parameterProviderId,
 parameterGroupConfigurations);
 
         final Set<AffectedComponentEntity> affectedComponents = 
getAffectedComponentEntities(parameterContextUpdates);
         logger.debug("Received Apply Request for Parameter Provider: {}; the 
following {} components will be affected: {}", requestEntity, 
affectedComponents.size(), affectedComponents);
 
         final InitiateParameterProviderApplyParametersRequestWrapper 
requestWrapper = new 
InitiateParameterProviderApplyParametersRequestWrapper(parameterProviderId,
-                parameterContextUpdates, componentLifecycle, 
getAbsolutePath(), affectedComponents, replicateRequest, user);
+                parameterGroupConfigurations, componentLifecycle, 
getAbsolutePath(), replicateRequest, user);
 
         final Revision requestRevision = 
getRevision(requestEntity.getRevision(), parameterProviderId);
         return withWriteLock(
@@ -988,15 +987,13 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
                 requestWrapper,
                 requestRevision,
                 lookup -> {
-                    // Verify READ permissions for user, for the Parameter 
Provider itself
-                    final ComponentAuthorizable parameterProvider = 
lookup.getParameterProvider(parameterProviderId);
-                    parameterProvider.getAuthorizable().authorize(authorizer, 
RequestAction.READ, user);
-
-                    parameterContextUpdates.forEach(context -> {
-                        final Authorizable parameterContext = 
lookup.getParameterContext(context.getComponent().getId());
-                        parameterContext.authorize(authorizer, 
RequestAction.READ, NiFiUserUtils.getNiFiUser());
-                        parameterContext.authorize(authorizer, 
RequestAction.WRITE, NiFiUserUtils.getNiFiUser());
-                    });
+                    AuthorizeParameterProviderApply.authorizeApplyParameters(
+                            parameterProviderId,
+                            requiresParameterContextCreation,
+                            parameterContextUpdates,
+                            authorizer,
+                            lookup, user
+                    );
 
                     // Verify READ and WRITE permissions for user, for every 
component that is affected
                     affectedComponents.forEach(component -> 
parameterUpdateManager.authorizeAffectedComponent(component, lookup, user, 
true, true));
@@ -1584,19 +1581,49 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
     }
 
     private Response submitApplyRequest(final Revision requestRevision, final 
InitiateParameterProviderApplyParametersRequestWrapper requestWrapper) {
+        final String parameterProviderId = 
requestWrapper.getParameterProviderId();
+        final NiFiUser user = requestWrapper.getUser();
+        final Collection<ParameterGroupConfiguration> 
parameterGroupConfigurations = requestWrapper.getParameterGroupConfigurations();
+
+        final List<ParameterGroupConfiguration> 
parameterGroupConfigurationsToCreate = parameterGroupConfigurations.stream()
+                .filter(parameterGroupConfiguration -> 
requiresNewParameterContext(parameterGroupConfiguration, user))
+                .toList();
+
+        // Authorize Parameter Context creation against the current flow state 
before creating, then create.
+        if (!parameterGroupConfigurationsToCreate.isEmpty()) {
+            serviceFacade.authorizeAccess(lookup -> 
lookup.getParameterContexts().authorize(authorizer, RequestAction.WRITE, user));
+
+            for (final ParameterGroupConfiguration parameterGroupConfiguration 
: parameterGroupConfigurationsToCreate) {
+                final ParameterContextEntity newParameterContext = 
getNewParameterContextEntity(parameterProviderId, parameterGroupConfiguration);
+                try {
+                    performParameterContextCreate(user, 
requestWrapper.getExampleUri(), requestWrapper.isReplicateRequest(), 
newParameterContext);
+                } catch (final LifecycleManagementException e) {
+                    throw new RuntimeException("Failed to create Parameter 
Context " + parameterGroupConfiguration.getGroupName(), e);
+                }
+            }
+        }
+
+        final List<ParameterContextEntity> parameterContextUpdates = 
serviceFacade.getParameterContextUpdatesForAppliedParameters(parameterProviderId,
 parameterGroupConfigurations);
+        final Set<AffectedComponentEntity> affectedComponents = 
getAffectedComponentEntities(parameterContextUpdates);
+
+        // Authorize the Parameter Contexts and affected components that will 
actually be updated after any creates.
+        serviceFacade.authorizeAccess(lookup -> {
+            
AuthorizeParameterProviderApply.authorizeApplyParameters(parameterProviderId, 
false, parameterContextUpdates, authorizer, lookup, user);
+            affectedComponents.forEach(component -> 
parameterUpdateManager.authorizeAffectedComponent(component, lookup, user, 
true, true));
+        });
+
         // Create an asynchronous request that will occur in the background, 
because this request may
         // result in stopping components, which can take an indeterminate 
amount of time.
         final String requestId = UUID.randomUUID().toString();
-        final String parameterProviderId = 
requestWrapper.getParameterProviderId();
 
         final AsynchronousWebRequest<List<ParameterContextEntity>, 
List<ParameterContextEntity>> request = new StandardAsynchronousWebRequest<>(
-                requestId, requestWrapper.getParameterContextEntities(), 
parameterProviderId, requestWrapper.getUser(), getUpdateSteps());
+                requestId, parameterContextUpdates, parameterProviderId, user, 
getUpdateSteps());
 
         // Submit the request to be performed in the background
         final Consumer<AsynchronousWebRequest<List<ParameterContextEntity>, 
List<ParameterContextEntity>>> updateTask = asyncRequest -> {
             try {
                 final List<ParameterContextEntity> 
updatedParameterContextEntities = 
parameterUpdateManager.updateParameterContexts(asyncRequest, 
requestWrapper.getComponentLifecycle(),
-                        requestWrapper.getExampleUri(), 
requestWrapper.getReferencingComponents(), requestWrapper.isReplicateRequest(), 
requestRevision, requestWrapper.getParameterContextEntities());
+                        requestWrapper.getExampleUri(), affectedComponents, 
requestWrapper.isReplicateRequest(), requestRevision, parameterContextUpdates);
 
                 asyncRequest.markStepComplete(updatedParameterContextEntities);
             } catch (final ResumeFlowException rfe) {
@@ -1697,22 +1724,21 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
 
     private static class 
InitiateParameterProviderApplyParametersRequestWrapper extends Entity {
         private final String parameterProviderId;
-        private final List<ParameterContextEntity> parameterContextEntities;
+        private final Collection<ParameterGroupConfiguration> 
parameterGroupConfigurations;
         private final ComponentLifecycle componentLifecycle;
         private final URI exampleUri;
-        private final Set<AffectedComponentEntity> affectedComponents;
         private final boolean replicateRequest;
         private final NiFiUser nifiUser;
 
-        public InitiateParameterProviderApplyParametersRequestWrapper(final 
String parameterProviderId, final List<ParameterContextEntity> 
parameterContextEntities,
+        public InitiateParameterProviderApplyParametersRequestWrapper(final 
String parameterProviderId,
+                                                                      final 
Collection<ParameterGroupConfiguration> parameterGroupConfigurations,
                                                                       final 
ComponentLifecycle componentLifecycle,
-                                                                      final 
URI exampleUri, final Set<AffectedComponentEntity> affectedComponents, final 
boolean replicateRequest,
+                                                                      final 
URI exampleUri, final boolean replicateRequest,
                                                                       final 
NiFiUser nifiUser) {
             this.parameterProviderId = parameterProviderId;
-            this.parameterContextEntities = parameterContextEntities;
+            this.parameterGroupConfigurations = parameterGroupConfigurations;
             this.componentLifecycle = componentLifecycle;
             this.exampleUri = exampleUri;
-            this.affectedComponents = affectedComponents;
             this.replicateRequest = replicateRequest;
             this.nifiUser = nifiUser;
         }
@@ -1721,8 +1747,8 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
             return parameterProviderId;
         }
 
-        public List<ParameterContextEntity> getParameterContextEntities() {
-            return parameterContextEntities;
+        public Collection<ParameterGroupConfiguration> 
getParameterGroupConfigurations() {
+            return parameterGroupConfigurations;
         }
 
         public ComponentLifecycle getComponentLifecycle() {
@@ -1733,10 +1759,6 @@ public class ParameterProviderResource extends 
AbstractParameterResource {
             return exampleUri;
         }
 
-        public Set<AffectedComponentEntity> getReferencingComponents() {
-            return affectedComponents;
-        }
-
         public boolean isReplicateRequest() {
             return replicateRequest;
         }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeParameterProviderApplyTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeParameterProviderApplyTest.java
new file mode 100644
index 00000000000..8ff769535db
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/authorization/AuthorizeParameterProviderApplyTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.parameter.ParameterContext;
+import org.apache.nifi.web.api.dto.ParameterContextDTO;
+import org.apache.nifi.web.api.entity.ParameterContextEntity;
+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.eq;
+import static org.mockito.Mockito.doNothing;
+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 AuthorizeParameterProviderApplyTest {
+
+    private static final String PARAMETER_PROVIDER_ID = 
"parameter-provider-id";
+
+    private static final String PARAMETER_CONTEXT_ID = "parameter-context-id";
+
+    @Mock
+    private Authorizer authorizer;
+
+    @Mock
+    private AuthorizableLookup lookup;
+
+    @Mock
+    private NiFiUser user;
+
+    @Mock
+    private ComponentAuthorizable parameterProviderAuthorizable;
+
+    @Mock
+    private Authorizable parameterProviderResource;
+
+    @Mock
+    private Authorizable parameterContexts;
+
+    @Mock
+    private ParameterContext parameterContext;
+
+    @Test
+    void testAuthorizeApplyParametersRequiresWriteToCreateParameterContext() {
+        
when(lookup.getParameterProvider(PARAMETER_PROVIDER_ID)).thenReturn(parameterProviderAuthorizable);
+        
when(parameterProviderAuthorizable.getAuthorizable()).thenReturn(parameterProviderResource);
+        when(lookup.getParameterContexts()).thenReturn(parameterContexts);
+        doThrow(new AccessDeniedException("Unable to modify parameter 
contexts")).when(parameterContexts)
+                .authorize(eq(authorizer), eq(RequestAction.WRITE), eq(user));
+
+        assertThrows(AccessDeniedException.class, () -> 
AuthorizeParameterProviderApply.authorizeApplyParameters(
+                PARAMETER_PROVIDER_ID, true, List.of(), authorizer, lookup, 
user));
+
+        verify(parameterProviderResource).authorize(eq(authorizer), 
eq(RequestAction.READ), eq(user));
+        verify(parameterContexts).authorize(eq(authorizer), 
eq(RequestAction.WRITE), eq(user));
+    }
+
+    @Test
+    void 
testAuthorizeApplyParametersDoesNotRequireWriteToCreateWhenNoNewParameterContext()
 {
+        
when(lookup.getParameterProvider(PARAMETER_PROVIDER_ID)).thenReturn(parameterProviderAuthorizable);
+        
when(parameterProviderAuthorizable.getAuthorizable()).thenReturn(parameterProviderResource);
+
+        
AuthorizeParameterProviderApply.authorizeApplyParameters(PARAMETER_PROVIDER_ID, 
false, List.of(), authorizer, lookup, user);
+
+        verify(parameterProviderResource).authorize(eq(authorizer), 
eq(RequestAction.READ), eq(user));
+        verify(lookup, never()).getParameterContexts();
+    }
+
+    @Test
+    void 
testAuthorizeApplyParametersRequiresReadAndWriteOnExistingParameterContext() {
+        
when(lookup.getParameterProvider(PARAMETER_PROVIDER_ID)).thenReturn(parameterProviderAuthorizable);
+        
when(parameterProviderAuthorizable.getAuthorizable()).thenReturn(parameterProviderResource);
+        
when(lookup.getParameterContext(PARAMETER_CONTEXT_ID)).thenReturn(parameterContext);
+        doNothing().when(parameterContext).authorize(eq(authorizer), 
eq(RequestAction.READ), eq(user));
+        doThrow(new AccessDeniedException("Unable to write Parameter 
Context")).when(parameterContext)
+                .authorize(eq(authorizer), eq(RequestAction.WRITE), eq(user));
+
+        assertThrows(AccessDeniedException.class, () -> 
AuthorizeParameterProviderApply.authorizeApplyParameters(
+                PARAMETER_PROVIDER_ID, false, 
List.of(parameterContextEntity()), authorizer, lookup, user));
+
+        verify(parameterContext).authorize(eq(authorizer), 
eq(RequestAction.READ), eq(user));
+        verify(parameterContext).authorize(eq(authorizer), 
eq(RequestAction.WRITE), eq(user));
+        verify(lookup, never()).getParameterContexts();
+    }
+
+    private static ParameterContextEntity parameterContextEntity() {
+        final ParameterContextDTO parameterContextDto = new 
ParameterContextDTO();
+        parameterContextDto.setId(PARAMETER_CONTEXT_ID);
+
+        final ParameterContextEntity parameterContextEntity = new 
ParameterContextEntity();
+        parameterContextEntity.setComponent(parameterContextDto);
+        return parameterContextEntity;
+    }
+}

Reply via email to