This is an automated email from the ASF dual-hosted git repository.
mcgilman pushed a commit to branch NIFI-15258
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/NIFI-15258 by this push:
new 0a861bea8b NIFI-15356: Adding authorization to the
StandardNiFiConnectorWebContext. (#10660)
0a861bea8b is described below
commit 0a861bea8bd0a898e789ff3f6fd62870fde4541b
Author: Matt Gilman <[email protected]>
AuthorDate: Mon Dec 22 09:01:28 2025 -0500
NIFI-15356: Adding authorization to the StandardNiFiConnectorWebContext.
(#10660)
---
.../nifi/web/api/entity/NarDetailsEntity.java | 10 +
.../java/org/apache/nifi/nar/NarInstallTask.java | 4 +-
.../nifi/web/StandardNiFiConnectorWebContext.java | 44 ----
.../apache/nifi/web/StandardNiFiServiceFacade.java | 2 +
.../configuration/WebApplicationConfiguration.java | 12 +-
.../connector/StandardNiFiConnectorWebContext.java | 99 +++++++++
.../authorization/AuthorizingConnectionFacade.java | 55 +++++
.../AuthorizingConnectorInvocationHandler.java | 130 +++++++++++
.../AuthorizingControllerServiceFacade.java | 99 +++++++++
.../AuthorizingControllerServiceLifecycle.java | 56 +++++
.../authorization/AuthorizingFlowContext.java | 70 ++++++
.../AuthorizingParameterContextFacade.java | 72 +++++++
.../AuthorizingProcessGroupFacade.java | 145 +++++++++++++
.../AuthorizingProcessGroupLifecycle.java | 88 ++++++++
.../authorization/AuthorizingProcessorFacade.java | 99 +++++++++
.../AuthorizingProcessorLifecycle.java | 80 +++++++
.../AuthorizingStatelessGroupLifecycle.java | 55 +++++
.../ConnectorAuthorizationContext.java | 74 +++++++
.../StandardNiFiConnectorWebContextTest.java | 219 +++++++++++++++++++
.../AuthorizingConnectorInvocationHandlerTest.java | 239 +++++++++++++++++++++
.../authorization/AuthorizingFlowContextTest.java | 167 ++++++++++++++
.../AuthorizingParameterContextFacadeTest.java | 162 ++++++++++++++
22 files changed, 1935 insertions(+), 46 deletions(-)
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/NarDetailsEntity.java
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/NarDetailsEntity.java
index 799677beee..35c4735e47 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/NarDetailsEntity.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/NarDetailsEntity.java
@@ -36,6 +36,7 @@ public class NarDetailsEntity extends Entity {
private Set<DocumentedTypeDTO> parameterProviderTypes;
private Set<DocumentedTypeDTO> flowRegistryClientTypes;
private Set<DocumentedTypeDTO> flowAnalysisRuleTypes;
+ private Set<DocumentedTypeDTO> connectorTypes;
@Schema(description = "The NAR summary")
public NarSummaryDTO getNarSummary() {
@@ -108,4 +109,13 @@ public class NarDetailsEntity extends Entity {
public void setFlowAnalysisRuleTypes(final Set<DocumentedTypeDTO>
flowAnalysisRuleTypes) {
this.flowAnalysisRuleTypes = flowAnalysisRuleTypes;
}
+
+ @Schema(description = "The Connector types contained in the NAR")
+ public Set<DocumentedTypeDTO> getConnectorTypes() {
+ return connectorTypes;
+ }
+
+ public void setConnectorTypes(final Set<DocumentedTypeDTO> connectorTypes)
{
+ this.connectorTypes = connectorTypes;
+ }
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/nar/NarInstallTask.java
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/nar/NarInstallTask.java
index 8c2673353d..ffdb2fa435 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/nar/NarInstallTask.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/nar/NarInstallTask.java
@@ -20,6 +20,7 @@ package org.apache.nifi.nar;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.bundle.BundleDetails;
+import org.apache.nifi.components.connector.Connector;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.controller.service.ControllerServiceProvider;
import org.apache.nifi.flowanalysis.FlowAnalysisRule;
@@ -54,7 +55,8 @@ public class NarInstallTask implements Runnable {
ReportingTask.class,
FlowRegistryClient.class,
FlowAnalysisRule.class,
- ParameterProvider.class
+ ParameterProvider.class,
+ Connector.class
);
private final NarNode narNode;
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiConnectorWebContext.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiConnectorWebContext.java
deleted file mode 100644
index 67c011330a..0000000000
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiConnectorWebContext.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.web;
-
-import org.apache.nifi.components.connector.ConnectorNode;
-import org.apache.nifi.web.dao.ConnectorDAO;
-
-/**
- * Implements the NiFiConnectorWebContext interface to provide
- * Connector instances to connector custom UIs.
- */
-public class StandardNiFiConnectorWebContext implements
NiFiConnectorWebContext {
-
- private ConnectorDAO connectorDAO;
-
- @Override
- @SuppressWarnings("unchecked")
- public <T> ConnectorWebContext<T> getConnectorWebContext(final String
connectorId) throws IllegalArgumentException {
- final ConnectorNode connectorNode =
connectorDAO.getConnector(connectorId);
- if (connectorNode == null) {
- throw new IllegalArgumentException("Unable to find connector with
id: " + connectorId);
- }
- return new ConnectorWebContext<>((T) connectorNode.getConnector(),
connectorNode.getWorkingFlowContext(), connectorNode.getActiveFlowContext());
- }
-
- public void setConnectorDAO(final ConnectorDAO connectorDAO) {
- this.connectorDAO = connectorDAO;
- }
-}
-
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index a472a18178..d988b937ed 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -78,6 +78,7 @@ import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.RequiredPermission;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.components.Validator;
+import org.apache.nifi.components.connector.Connector;
import org.apache.nifi.components.connector.ConnectorNode;
import org.apache.nifi.components.connector.ConnectorUpdateContext;
import org.apache.nifi.components.connector.Secret;
@@ -7369,6 +7370,7 @@ public class StandardNiFiServiceFacade implements
NiFiServiceFacade {
componentTypesEntity.setParameterProviderTypes(dtoFactory.fromDocumentedTypes(getTypes(extensionDefinitions,
ParameterProvider.class)));
componentTypesEntity.setFlowRegistryClientTypes(dtoFactory.fromDocumentedTypes(getTypes(extensionDefinitions,
FlowRegistryClient.class)));
componentTypesEntity.setFlowAnalysisRuleTypes(dtoFactory.fromDocumentedTypes(getTypes(extensionDefinitions,
FlowAnalysisRule.class)));
+
componentTypesEntity.setConnectorTypes(dtoFactory.fromDocumentedTypes(getTypes(extensionDefinitions,
Connector.class)));
return componentTypesEntity;
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/configuration/WebApplicationConfiguration.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/configuration/WebApplicationConfiguration.java
index 0659d8ef72..ef53f7b877 100644
---
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/configuration/WebApplicationConfiguration.java
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/configuration/WebApplicationConfiguration.java
@@ -18,6 +18,7 @@ package org.apache.nifi.web.configuration;
import org.apache.nifi.admin.service.AuditService;
import org.apache.nifi.audit.NiFiAuditor;
+import org.apache.nifi.authorization.AuthorizableLookup;
import org.apache.nifi.authorization.Authorizer;
import org.apache.nifi.authorization.StandardAuthorizableLookup;
import org.apache.nifi.cluster.coordination.ClusterCoordinator;
@@ -35,7 +36,7 @@ import org.apache.nifi.web.NiFiServiceFacade;
import org.apache.nifi.web.NiFiServiceFacadeLock;
import org.apache.nifi.web.NiFiConnectorWebContext;
import org.apache.nifi.web.NiFiWebConfigurationContext;
-import org.apache.nifi.web.StandardNiFiConnectorWebContext;
+import org.apache.nifi.web.connector.StandardNiFiConnectorWebContext;
import org.apache.nifi.web.StandardNiFiContentAccess;
import org.apache.nifi.web.StandardNiFiServiceFacade;
import org.apache.nifi.web.StandardNiFiWebConfigurationContext;
@@ -109,6 +110,8 @@ public class WebApplicationConfiguration {
private ConnectorDAO connectorDAO;
+ private AuthorizableLookup authorizableLookup;
+
public WebApplicationConfiguration(
final Authorizer authorizer,
final AccessPolicyDAO accessPolicyDao,
@@ -155,6 +158,11 @@ public class WebApplicationConfiguration {
this.connectorDAO = connectorDAO;
}
+ @Autowired
+ public void setAuthorizableLookup(final AuthorizableLookup
authorizableLookup) {
+ this.authorizableLookup = authorizableLookup;
+ }
+
@Bean
public EntityFactory entityFactory() {
return new EntityFactory();
@@ -270,6 +278,8 @@ public class WebApplicationConfiguration {
public NiFiConnectorWebContext nifiConnectorWebContext() {
final StandardNiFiConnectorWebContext context = new
StandardNiFiConnectorWebContext();
context.setConnectorDAO(connectorDAO);
+ context.setAuthorizer(authorizer);
+ context.setAuthorizableLookup(authorizableLookup);
return context;
}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContext.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContext.java
new file mode 100644
index 0000000000..5b5018174c
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContext.java
@@ -0,0 +1,99 @@
+/*
+ * 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.web.connector;
+
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.components.connector.ConnectorNode;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.web.NiFiConnectorWebContext;
+import
org.apache.nifi.web.connector.authorization.AuthorizingConnectorInvocationHandler;
+import org.apache.nifi.web.connector.authorization.AuthorizingFlowContext;
+import
org.apache.nifi.web.connector.authorization.ConnectorAuthorizationContext;
+import org.apache.nifi.web.dao.ConnectorDAO;
+
+import java.lang.reflect.Proxy;
+
+/**
+ * Implements the NiFiConnectorWebContext interface to provide
+ * Connector instances to connector custom UIs.
+ *
+ * <p>The returned Connector instance is wrapped in an authorization proxy that
+ * enforces permissions based on the {@link ConnectorWebMethod} annotation on
+ * the connector interface methods. Methods without this annotation cannot be
+ * invoked through the proxy.</p>
+ *
+ * <p>The returned FlowContext instances are also wrapped in authorization
wrappers
+ * that enforce read/write permissions on all operations.</p>
+ */
+public class StandardNiFiConnectorWebContext implements
NiFiConnectorWebContext {
+
+ private ConnectorDAO connectorDAO;
+ private Authorizer authorizer;
+ private AuthorizableLookup authorizableLookup;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T> ConnectorWebContext<T> getConnectorWebContext(final String
connectorId) throws IllegalArgumentException {
+ final ConnectorNode connectorNode =
connectorDAO.getConnector(connectorId);
+ if (connectorNode == null) {
+ throw new IllegalArgumentException("Unable to find connector with
id: " + connectorId);
+ }
+
+ final ConnectorAuthorizationContext authContext = new
ConnectorAuthorizationContext(connectorId, authorizer, authorizableLookup);
+
+ final T connector = (T) connectorNode.getConnector();
+ final T authorizedConnectorProxy = createAuthorizingProxy(connector,
connectorId);
+
+ final FlowContext workingFlowContext = new
AuthorizingFlowContext(connectorNode.getWorkingFlowContext(), authContext);
+ final FlowContext activeFlowContext = new
AuthorizingFlowContext(connectorNode.getActiveFlowContext(), authContext);
+
+ return new ConnectorWebContext<>(authorizedConnectorProxy,
workingFlowContext, activeFlowContext);
+ }
+
+ /**
+ * Creates a proxy around the given connector that enforces authorization
+ * based on {@link ConnectorWebMethod} annotations.
+ *
+ * @param <T> the type of the connector
+ * @param connector the connector instance to wrap
+ * @param connectorId the ID of the connector
+ * @return a proxy that enforces authorization on method invocations
+ */
+ @SuppressWarnings("unchecked")
+ private <T> T createAuthorizingProxy(final T connector, final String
connectorId) {
+ final AuthorizingConnectorInvocationHandler<T> handler = new
AuthorizingConnectorInvocationHandler<>(
+ connector, connectorId, authorizer, authorizableLookup);
+
+ return (T) Proxy.newProxyInstance(
+ connector.getClass().getClassLoader(),
+ connector.getClass().getInterfaces(),
+ handler);
+ }
+
+ public void setConnectorDAO(final ConnectorDAO connectorDAO) {
+ this.connectorDAO = connectorDAO;
+ }
+
+ public void setAuthorizer(final Authorizer authorizer) {
+ this.authorizer = authorizer;
+ }
+
+ public void setAuthorizableLookup(final AuthorizableLookup
authorizableLookup) {
+ this.authorizableLookup = authorizableLookup;
+ }
+}
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectionFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectionFacade.java
new file mode 100644
index 0000000000..f96642e2b7
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectionFacade.java
@@ -0,0 +1,55 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.connector.components.ConnectionFacade;
+import org.apache.nifi.controller.queue.QueueSize;
+import org.apache.nifi.flow.VersionedConnection;
+
+/**
+ * A wrapper around {@link ConnectionFacade} that enforces authorization
before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingConnectionFacade implements ConnectionFacade {
+
+ private final ConnectionFacade delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingConnectionFacade(final ConnectionFacade delegate, final
ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public VersionedConnection getDefinition() {
+ authContext.authorizeRead();
+ return delegate.getDefinition();
+ }
+
+ @Override
+ public QueueSize getQueueSize() {
+ authContext.authorizeRead();
+ return delegate.getQueueSize();
+ }
+
+ @Override
+ public void purge() {
+ authContext.authorizeWrite();
+ delegate.purge();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandler.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandler.java
new file mode 100644
index 0000000000..130fa43248
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandler.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.web.connector.authorization;
+
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.web.ConnectorWebMethod;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An InvocationHandler that wraps a Connector instance and enforces
authorization
+ * based on the {@link ConnectorWebMethod} annotation present on the invoked
method.
+ *
+ * <p>Methods must be annotated with {@link ConnectorWebMethod} to be
invokable through
+ * this handler. The annotation specifies whether READ or WRITE access is
required.
+ * Methods without the annotation will result in an {@link
IllegalStateException}.</p>
+ *
+ * @param <T> the type of the Connector being proxied
+ */
+public class AuthorizingConnectorInvocationHandler<T> implements
InvocationHandler {
+
+ private final T delegate;
+ private final String connectorId;
+ private final Authorizer authorizer;
+ private final AuthorizableLookup authorizableLookup;
+
+ /**
+ * Constructs an AuthorizingConnectorInvocationHandler.
+ *
+ * @param delegate the actual Connector instance to delegate method calls
to
+ * @param connectorId the ID of the connector, used for authorization
lookups
+ * @param authorizer the Authorizer to use for authorization checks
+ * @param authorizableLookup the lookup service to obtain the Authorizable
for the connector
+ */
+ public AuthorizingConnectorInvocationHandler(final T delegate, final
String connectorId,
+ final Authorizer authorizer,
final AuthorizableLookup authorizableLookup) {
+ this.delegate = delegate;
+ this.connectorId = connectorId;
+ this.authorizer = authorizer;
+ this.authorizableLookup = authorizableLookup;
+ }
+
+ @Override
+ public Object invoke(final Object proxy, final Method method, final
Object[] args) throws Throwable {
+ final ConnectorWebMethod annotation =
findConnectorWebMethodAnnotation(method);
+
+ if (annotation == null) {
+ throw new IllegalStateException(String.format(
+ "Method [%s] on connector [%s] is not annotated with
@ConnectorWebMethod and cannot be invoked through the Connector Web Context",
+ method.getName(), connectorId));
+ }
+
+ final RequestAction requiredAction =
mapAccessTypeToRequestAction(annotation.value());
+ final Authorizable connector =
authorizableLookup.getConnector(connectorId);
+ final NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+ connector.authorize(authorizer, requiredAction, user);
+
+ try {
+ return method.invoke(delegate, args);
+ } catch (final InvocationTargetException e) {
+ throw e.getCause();
+ }
+ }
+
+ /**
+ * Maps the ConnectorWebMethod.AccessType to the corresponding
RequestAction.
+ *
+ * @param accessType the access type from the annotation
+ * @return the corresponding RequestAction
+ */
+ private RequestAction mapAccessTypeToRequestAction(final
ConnectorWebMethod.AccessType accessType) {
+ return switch (accessType) {
+ case READ -> RequestAction.READ;
+ case WRITE -> RequestAction.WRITE;
+ };
+ }
+
+ /**
+ * Finds the ConnectorWebMethod annotation on the given method. This
method searches
+ * the declaring class's interfaces to find the annotation, as the method
parameter
+ * may be from the proxy class rather than the interface.
+ *
+ * @param method the method to search for the annotation
+ * @return the ConnectorWebMethod annotation, or null if not found
+ */
+ private ConnectorWebMethod findConnectorWebMethodAnnotation(final Method
method) {
+ final ConnectorWebMethod directAnnotation =
method.getAnnotation(ConnectorWebMethod.class);
+ if (directAnnotation != null) {
+ return directAnnotation;
+ }
+
+ for (final Class<?> iface : delegate.getClass().getInterfaces()) {
+ try {
+ final Method interfaceMethod =
iface.getMethod(method.getName(), method.getParameterTypes());
+ final ConnectorWebMethod interfaceAnnotation =
interfaceMethod.getAnnotation(ConnectorWebMethod.class);
+ if (interfaceAnnotation != null) {
+ return interfaceAnnotation;
+ }
+ } catch (final NoSuchMethodException ignored) {
+ // Method not found on this interface; continue searching
other interfaces
+ continue;
+ }
+ }
+
+ return null;
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceFacade.java
new file mode 100644
index 0000000000..2086c875d4
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceFacade.java
@@ -0,0 +1,99 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.components.ControllerServiceFacade;
+import
org.apache.nifi.components.connector.components.ControllerServiceLifecycle;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedParameterContext;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper around {@link ControllerServiceFacade} that enforces
authorization before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingControllerServiceFacade implements
ControllerServiceFacade {
+
+ private final ControllerServiceFacade delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingControllerServiceFacade(final ControllerServiceFacade
delegate, final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public VersionedControllerService getDefinition() {
+ authContext.authorizeRead();
+ return delegate.getDefinition();
+ }
+
+ @Override
+ public ControllerServiceLifecycle getLifecycle() {
+ authContext.authorizeRead();
+ return new
AuthorizingControllerServiceLifecycle(delegate.getLifecycle(), authContext);
+ }
+
+ @Override
+ public List<ValidationResult> validate() {
+ authContext.authorizeRead();
+ return delegate.validate();
+ }
+
+ @Override
+ public List<ValidationResult> validate(final Map<String, String>
propertyValues) {
+ authContext.authorizeRead();
+ return delegate.validate(propertyValues);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final Map<String, String>
propertyValues, final Map<String, String> variables) {
+ authContext.authorizeRead();
+ return delegate.verify(propertyValues, variables);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final Map<String, String>
propertyValues, final VersionedParameterContext parameterContext, final
Map<String, String> variables) {
+ authContext.authorizeRead();
+ return delegate.verify(propertyValues, parameterContext, variables);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final VersionedExternalFlow
versionedExternalFlow, final Map<String, String> variables) {
+ authContext.authorizeRead();
+ return delegate.verify(versionedExternalFlow, variables);
+ }
+
+ @Override
+ public Object invokeConnectorMethod(final String methodName, final
Map<String, Object> arguments) throws InvocationFailedException {
+ authContext.authorizeWrite();
+ return delegate.invokeConnectorMethod(methodName, arguments);
+ }
+
+ @Override
+ public <T> T invokeConnectorMethod(final String methodName, final
Map<String, Object> arguments, final Class<T> returnType) throws
InvocationFailedException {
+ authContext.authorizeWrite();
+ return delegate.invokeConnectorMethod(methodName, arguments,
returnType);
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceLifecycle.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceLifecycle.java
new file mode 100644
index 0000000000..838632fdfb
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingControllerServiceLifecycle.java
@@ -0,0 +1,56 @@
+/*
+ * 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.web.connector.authorization;
+
+import
org.apache.nifi.components.connector.components.ControllerServiceLifecycle;
+import org.apache.nifi.components.connector.components.ControllerServiceState;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A wrapper around {@link ControllerServiceLifecycle} that enforces
authorization before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingControllerServiceLifecycle implements
ControllerServiceLifecycle {
+
+ private final ControllerServiceLifecycle delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingControllerServiceLifecycle(final
ControllerServiceLifecycle delegate, final ConnectorAuthorizationContext
authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public ControllerServiceState getState() {
+ authContext.authorizeRead();
+ return delegate.getState();
+ }
+
+ @Override
+ public CompletableFuture<Void> enable() {
+ authContext.authorizeWrite();
+ return delegate.enable();
+ }
+
+ @Override
+ public CompletableFuture<Void> disable() {
+ authContext.authorizeWrite();
+ return delegate.disable();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContext.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContext.java
new file mode 100644
index 0000000000..d03b5e71fb
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContext.java
@@ -0,0 +1,70 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.FlowContextType;
+import org.apache.nifi.components.connector.components.ParameterContextFacade;
+import org.apache.nifi.components.connector.components.ProcessGroupFacade;
+import org.apache.nifi.flow.Bundle;
+
+/**
+ * A wrapper around {@link FlowContext} that enforces authorization before
delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingFlowContext implements FlowContext {
+
+ private final FlowContext delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingFlowContext(final FlowContext delegate, final
ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public ProcessGroupFacade getRootGroup() {
+ authContext.authorizeRead();
+ return new AuthorizingProcessGroupFacade(delegate.getRootGroup(),
authContext);
+ }
+
+ @Override
+ public ParameterContextFacade getParameterContext() {
+ authContext.authorizeRead();
+ return new
AuthorizingParameterContextFacade(delegate.getParameterContext(), authContext);
+ }
+
+ @Override
+ public ConnectorConfigurationContext getConfigurationContext() {
+ authContext.authorizeRead();
+ return delegate.getConfigurationContext();
+ }
+
+ @Override
+ public FlowContextType getType() {
+ authContext.authorizeRead();
+ return delegate.getType();
+ }
+
+ @Override
+ public Bundle getBundle() {
+ authContext.authorizeRead();
+ return delegate.getBundle();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacade.java
new file mode 100644
index 0000000000..ead31b9481
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacade.java
@@ -0,0 +1,72 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.asset.Asset;
+import org.apache.nifi.components.connector.components.ParameterContextFacade;
+import org.apache.nifi.components.connector.components.ParameterValue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * A wrapper around {@link ParameterContextFacade} that enforces authorization
before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingParameterContextFacade implements
ParameterContextFacade {
+
+ private final ParameterContextFacade delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingParameterContextFacade(final ParameterContextFacade
delegate, final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public void updateParameters(final Collection<ParameterValue>
parameterValues) {
+ authContext.authorizeWrite();
+ delegate.updateParameters(parameterValues);
+ }
+
+ @Override
+ public String getValue(final String parameterName) {
+ authContext.authorizeRead();
+ return delegate.getValue(parameterName);
+ }
+
+ @Override
+ public Set<String> getDefinedParameterNames() {
+ authContext.authorizeRead();
+ return delegate.getDefinedParameterNames();
+ }
+
+ @Override
+ public boolean isSensitive(final String parameterName) {
+ authContext.authorizeRead();
+ return delegate.isSensitive(parameterName);
+ }
+
+ @Override
+ public Asset createAsset(final InputStream inputStream) throws IOException
{
+ authContext.authorizeWrite();
+ return delegate.createAsset(inputStream);
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupFacade.java
new file mode 100644
index 0000000000..21ade9740d
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupFacade.java
@@ -0,0 +1,145 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.connector.components.ConnectionFacade;
+import org.apache.nifi.components.connector.components.ControllerServiceFacade;
+import
org.apache.nifi.components.connector.components.ControllerServiceReferenceHierarchy;
+import
org.apache.nifi.components.connector.components.ControllerServiceReferenceScope;
+import org.apache.nifi.components.connector.components.ProcessGroupFacade;
+import org.apache.nifi.components.connector.components.ProcessGroupLifecycle;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.components.StatelessGroupLifecycle;
+import org.apache.nifi.controller.queue.QueueSize;
+import org.apache.nifi.flow.VersionedProcessGroup;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A wrapper around {@link ProcessGroupFacade} that enforces authorization
before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingProcessGroupFacade implements ProcessGroupFacade {
+
+ private final ProcessGroupFacade delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingProcessGroupFacade(final ProcessGroupFacade delegate,
final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public VersionedProcessGroup getDefinition() {
+ authContext.authorizeRead();
+ return delegate.getDefinition();
+ }
+
+ @Override
+ public ProcessorFacade getProcessor(final String id) {
+ authContext.authorizeRead();
+ final ProcessorFacade processor = delegate.getProcessor(id);
+ return processor == null ? null : new
AuthorizingProcessorFacade(processor, authContext);
+ }
+
+ @Override
+ public Set<ProcessorFacade> getProcessors() {
+ authContext.authorizeRead();
+ return delegate.getProcessors().stream()
+ .map(p -> new AuthorizingProcessorFacade(p, authContext))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public ControllerServiceFacade getControllerService(final String id) {
+ authContext.authorizeRead();
+ final ControllerServiceFacade service =
delegate.getControllerService(id);
+ return service == null ? null : new
AuthorizingControllerServiceFacade(service, authContext);
+ }
+
+ @Override
+ public Set<ControllerServiceFacade> getControllerServices() {
+ authContext.authorizeRead();
+ return delegate.getControllerServices().stream()
+ .map(s -> new AuthorizingControllerServiceFacade(s,
authContext))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<ControllerServiceFacade> getControllerServices(final
ControllerServiceReferenceScope referenceScope, final
ControllerServiceReferenceHierarchy hierarchy) {
+ authContext.authorizeRead();
+ return delegate.getControllerServices(referenceScope,
hierarchy).stream()
+ .map(s -> new AuthorizingControllerServiceFacade(s,
authContext))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public ConnectionFacade getConnection(final String id) {
+ authContext.authorizeRead();
+ final ConnectionFacade connection = delegate.getConnection(id);
+ return connection == null ? null : new
AuthorizingConnectionFacade(connection, authContext);
+ }
+
+ @Override
+ public Set<ConnectionFacade> getConnections() {
+ authContext.authorizeRead();
+ return delegate.getConnections().stream()
+ .map(c -> new AuthorizingConnectionFacade(c, authContext))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public ProcessGroupFacade getProcessGroup(final String id) {
+ authContext.authorizeRead();
+ final ProcessGroupFacade group = delegate.getProcessGroup(id);
+ return group == null ? null : new AuthorizingProcessGroupFacade(group,
authContext);
+ }
+
+ @Override
+ public Set<ProcessGroupFacade> getProcessGroups() {
+ authContext.authorizeRead();
+ return delegate.getProcessGroups().stream()
+ .map(g -> new AuthorizingProcessGroupFacade(g, authContext))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public QueueSize getQueueSize() {
+ authContext.authorizeRead();
+ return delegate.getQueueSize();
+ }
+
+ @Override
+ public boolean isFlowEmpty() {
+ authContext.authorizeRead();
+ return delegate.isFlowEmpty();
+ }
+
+ @Override
+ public StatelessGroupLifecycle getStatelessLifecycle() {
+ authContext.authorizeRead();
+ return new
AuthorizingStatelessGroupLifecycle(delegate.getStatelessLifecycle(),
authContext);
+ }
+
+ @Override
+ public ProcessGroupLifecycle getLifecycle() {
+ authContext.authorizeRead();
+ return new AuthorizingProcessGroupLifecycle(delegate.getLifecycle(),
authContext);
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupLifecycle.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupLifecycle.java
new file mode 100644
index 0000000000..d29a056425
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessGroupLifecycle.java
@@ -0,0 +1,88 @@
+/*
+ * 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.web.connector.authorization;
+
+import
org.apache.nifi.components.connector.components.ControllerServiceReferenceHierarchy;
+import
org.apache.nifi.components.connector.components.ControllerServiceReferenceScope;
+import org.apache.nifi.components.connector.components.ProcessGroupLifecycle;
+
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A wrapper around {@link ProcessGroupLifecycle} that enforces authorization
before delegating
+ * to the underlying implementation. All lifecycle operations require WRITE
authorization.
+ */
+public class AuthorizingProcessGroupLifecycle implements ProcessGroupLifecycle
{
+
+ private final ProcessGroupLifecycle delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingProcessGroupLifecycle(final ProcessGroupLifecycle
delegate, final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public CompletableFuture<Void> enableControllerServices(final
ControllerServiceReferenceScope scope, final
ControllerServiceReferenceHierarchy hierarchy) {
+ authContext.authorizeWrite();
+ return delegate.enableControllerServices(scope, hierarchy);
+ }
+
+ @Override
+ public CompletableFuture<Void> enableControllerServices(final
Collection<String> serviceIdentifiers) {
+ authContext.authorizeWrite();
+ return delegate.enableControllerServices(serviceIdentifiers);
+ }
+
+ @Override
+ public CompletableFuture<Void> disableControllerServices(final
ControllerServiceReferenceHierarchy hierarchy) {
+ authContext.authorizeWrite();
+ return delegate.disableControllerServices(hierarchy);
+ }
+
+ @Override
+ public CompletableFuture<Void> disableControllerServices(final
Collection<String> serviceIdentifiers) {
+ authContext.authorizeWrite();
+ return delegate.disableControllerServices(serviceIdentifiers);
+ }
+
+ @Override
+ public CompletableFuture<Void> startProcessors() {
+ authContext.authorizeWrite();
+ return delegate.startProcessors();
+ }
+
+ @Override
+ public CompletableFuture<Void> start(final ControllerServiceReferenceScope
serviceReferenceScope) {
+ authContext.authorizeWrite();
+ return delegate.start(serviceReferenceScope);
+ }
+
+ @Override
+ public CompletableFuture<Void> stop() {
+ authContext.authorizeWrite();
+ return delegate.stop();
+ }
+
+ @Override
+ public CompletableFuture<Void> stopProcessors() {
+ authContext.authorizeWrite();
+ return delegate.stopProcessors();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorFacade.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorFacade.java
new file mode 100644
index 0000000000..22fe562230
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorFacade.java
@@ -0,0 +1,99 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.components.ProcessorLifecycle;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedParameterContext;
+import org.apache.nifi.flow.VersionedProcessor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A wrapper around {@link ProcessorFacade} that enforces authorization before
delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingProcessorFacade implements ProcessorFacade {
+
+ private final ProcessorFacade delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingProcessorFacade(final ProcessorFacade delegate, final
ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public VersionedProcessor getDefinition() {
+ authContext.authorizeRead();
+ return delegate.getDefinition();
+ }
+
+ @Override
+ public ProcessorLifecycle getLifecycle() {
+ authContext.authorizeRead();
+ return new AuthorizingProcessorLifecycle(delegate.getLifecycle(),
authContext);
+ }
+
+ @Override
+ public List<ValidationResult> validate() {
+ authContext.authorizeRead();
+ return delegate.validate();
+ }
+
+ @Override
+ public List<ValidationResult> validate(final Map<String, String>
propertyValues) {
+ authContext.authorizeRead();
+ return delegate.validate(propertyValues);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final Map<String, String>
propertyValues, final Map<String, String> attributes) {
+ authContext.authorizeRead();
+ return delegate.verify(propertyValues, attributes);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final Map<String, String>
propertyValues, final VersionedParameterContext parameterContext, final
Map<String, String> attributes) {
+ authContext.authorizeRead();
+ return delegate.verify(propertyValues, parameterContext, attributes);
+ }
+
+ @Override
+ public List<ConfigVerificationResult> verify(final VersionedExternalFlow
versionedExternalFlow, final Map<String, String> attributes) {
+ authContext.authorizeRead();
+ return delegate.verify(versionedExternalFlow, attributes);
+ }
+
+ @Override
+ public Object invokeConnectorMethod(final String methodName, final
Map<String, Object> arguments) throws InvocationFailedException {
+ authContext.authorizeWrite();
+ return delegate.invokeConnectorMethod(methodName, arguments);
+ }
+
+ @Override
+ public <T> T invokeConnectorMethod(final String methodName, final
Map<String, Object> arguments, final Class<T> returnType) throws
InvocationFailedException {
+ authContext.authorizeWrite();
+ return delegate.invokeConnectorMethod(methodName, arguments,
returnType);
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorLifecycle.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorLifecycle.java
new file mode 100644
index 0000000000..562551def8
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingProcessorLifecycle.java
@@ -0,0 +1,80 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.connector.components.ProcessorLifecycle;
+import org.apache.nifi.components.connector.components.ProcessorState;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A wrapper around {@link ProcessorLifecycle} that enforces authorization
before delegating
+ * to the underlying implementation.
+ */
+public class AuthorizingProcessorLifecycle implements ProcessorLifecycle {
+
+ private final ProcessorLifecycle delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingProcessorLifecycle(final ProcessorLifecycle delegate,
final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public ProcessorState getState() {
+ authContext.authorizeRead();
+ return delegate.getState();
+ }
+
+ @Override
+ public int getActiveThreadCount() {
+ authContext.authorizeRead();
+ return delegate.getActiveThreadCount();
+ }
+
+ @Override
+ public void terminate() {
+ authContext.authorizeWrite();
+ delegate.terminate();
+ }
+
+ @Override
+ public CompletableFuture<Void> stop() {
+ authContext.authorizeWrite();
+ return delegate.stop();
+ }
+
+ @Override
+ public CompletableFuture<Void> start() {
+ authContext.authorizeWrite();
+ return delegate.start();
+ }
+
+ @Override
+ public void disable() {
+ authContext.authorizeWrite();
+ delegate.disable();
+ }
+
+ @Override
+ public void enable() {
+ authContext.authorizeWrite();
+ delegate.enable();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingStatelessGroupLifecycle.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingStatelessGroupLifecycle.java
new file mode 100644
index 0000000000..bf822669a6
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/AuthorizingStatelessGroupLifecycle.java
@@ -0,0 +1,55 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.components.connector.components.StatelessGroupLifecycle;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A wrapper around {@link StatelessGroupLifecycle} that enforces
authorization before delegating
+ * to the underlying implementation. All lifecycle operations require WRITE
authorization.
+ */
+public class AuthorizingStatelessGroupLifecycle implements
StatelessGroupLifecycle {
+
+ private final StatelessGroupLifecycle delegate;
+ private final ConnectorAuthorizationContext authContext;
+
+ public AuthorizingStatelessGroupLifecycle(final StatelessGroupLifecycle
delegate, final ConnectorAuthorizationContext authContext) {
+ this.delegate = delegate;
+ this.authContext = authContext;
+ }
+
+ @Override
+ public CompletableFuture<Void> start() {
+ authContext.authorizeWrite();
+ return delegate.start();
+ }
+
+ @Override
+ public CompletableFuture<Void> stop() {
+ authContext.authorizeWrite();
+ return delegate.stop();
+ }
+
+ @Override
+ public CompletableFuture<Void> terminate() {
+ authContext.authorizeWrite();
+ return delegate.terminate();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/ConnectorAuthorizationContext.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/ConnectorAuthorizationContext.java
new file mode 100644
index 0000000000..00f9930047
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/connector/authorization/ConnectorAuthorizationContext.java
@@ -0,0 +1,74 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+
+/**
+ * Holds the authorization context needed to authorize operations on a
Connector's FlowContext
+ * and its associated facades.
+ */
+public class ConnectorAuthorizationContext {
+
+ private final String connectorId;
+ private final Authorizer authorizer;
+ private final AuthorizableLookup authorizableLookup;
+
+ public ConnectorAuthorizationContext(final String connectorId, final
Authorizer authorizer, final AuthorizableLookup authorizableLookup) {
+ this.connectorId = connectorId;
+ this.authorizer = authorizer;
+ this.authorizableLookup = authorizableLookup;
+ }
+
+ /**
+ * Authorizes the current user for read access to the connector.
+ */
+ public void authorizeRead() {
+ authorize(RequestAction.READ);
+ }
+
+ /**
+ * Authorizes the current user for write access to the connector.
+ */
+ public void authorizeWrite() {
+ authorize(RequestAction.WRITE);
+ }
+
+ private void authorize(final RequestAction action) {
+ final Authorizable connector =
authorizableLookup.getConnector(connectorId);
+ final NiFiUser user = NiFiUserUtils.getNiFiUser();
+ connector.authorize(authorizer, action, user);
+ }
+
+ public String getConnectorId() {
+ return connectorId;
+ }
+
+ public Authorizer getAuthorizer() {
+ return authorizer;
+ }
+
+ public AuthorizableLookup getAuthorizableLookup() {
+ return authorizableLookup;
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContextTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContextTest.java
new file mode 100644
index 0000000000..321e7ff02c
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/StandardNiFiConnectorWebContextTest.java
@@ -0,0 +1,219 @@
+/*
+ * 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.web.connector;
+
+import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.components.connector.Connector;
+import org.apache.nifi.web.ConnectorWebMethod;
+import org.apache.nifi.web.ConnectorWebMethod.AccessType;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizationRequest;
+import org.apache.nifi.authorization.AuthorizationResult;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.components.connector.ConnectorNode;
+import org.apache.nifi.components.connector.FrameworkFlowContext;
+import org.apache.nifi.web.NiFiConnectorWebContext.ConnectorWebContext;
+import org.apache.nifi.web.connector.authorization.AuthorizingFlowContext;
+import org.apache.nifi.web.dao.ConnectorDAO;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.lang.reflect.Proxy;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+@ExtendWith(MockitoExtension.class)
+public class StandardNiFiConnectorWebContextTest {
+
+ private static final String CONNECTOR_ID = "test-connector-id";
+ private static final String USER_IDENTITY = "test-user";
+
+ @Mock
+ private ConnectorDAO connectorDAO;
+
+ @Mock
+ private Authorizer authorizer;
+
+ @Mock
+ private AuthorizableLookup authorizableLookup;
+
+ @Mock
+ private ConnectorNode connectorNode;
+
+ @Mock
+ private FrameworkFlowContext workingFlowContext;
+
+ @Mock
+ private FrameworkFlowContext activeFlowContext;
+
+ @Mock
+ private Authorizable connectorAuthorizable;
+
+ @Mock
+ private SecurityContext securityContext;
+
+ @Mock
+ private Authentication authentication;
+
+ private StandardNiFiConnectorWebContext context;
+
+ private TestConnector testConnectorMock;
+ private String lastWrittenValue;
+
+ @BeforeEach
+ void setUp() {
+ context = new StandardNiFiConnectorWebContext();
+ context.setConnectorDAO(connectorDAO);
+ context.setAuthorizer(authorizer);
+ context.setAuthorizableLookup(authorizableLookup);
+
+ testConnectorMock = mock(TestConnector.class,
withSettings().extraInterfaces(Connector.class).lenient());
+ lenient().when(testConnectorMock.readData()).thenReturn("read-result");
+
lenient().when(testConnectorMock.writeData(any())).thenAnswer(invocation -> {
+ lastWrittenValue = invocation.getArgument(0);
+ return null;
+ });
+
+
lenient().when(connectorDAO.getConnector(CONNECTOR_ID)).thenReturn(connectorNode);
+ lenient().when(connectorNode.getConnector()).thenReturn((Connector)
testConnectorMock);
+
lenient().when(connectorNode.getWorkingFlowContext()).thenReturn(workingFlowContext);
+
lenient().when(connectorNode.getActiveFlowContext()).thenReturn(activeFlowContext);
+
lenient().when(authorizableLookup.getConnector(CONNECTOR_ID)).thenReturn(connectorAuthorizable);
+
lenient().when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
+
+ final NiFiUser user = new
StandardNiFiUser.Builder().identity(USER_IDENTITY).build();
+ final NiFiUserDetails userDetails = new NiFiUserDetails(user);
+ lenient().when(authentication.getPrincipal()).thenReturn(userDetails);
+
lenient().when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+ }
+
+ @AfterEach
+ void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void testGetConnectorWebContextReturnsProxiedConnector() {
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+
+ assertNotNull(webContext);
+ assertNotNull(webContext.connector());
+ assertTrue(Proxy.isProxyClass(webContext.connector().getClass()));
+ assertNotSame(testConnectorMock, webContext.connector());
+ }
+
+ @Test
+ void testGetConnectorWebContextReturnsWrappedFlowContexts() {
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+
+ assertNotNull(webContext.workingFlowContext());
+ assertNotNull(webContext.activeFlowContext());
+ assertTrue(webContext.workingFlowContext() instanceof
AuthorizingFlowContext);
+ assertTrue(webContext.activeFlowContext() instanceof
AuthorizingFlowContext);
+ }
+
+ @Test
+ void testGetConnectorWebContextThrowsForNonExistentConnector() {
+ when(connectorDAO.getConnector("non-existent-id")).thenReturn(null);
+
+ final IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class,
+ () -> context.getConnectorWebContext("non-existent-id"));
+ assertEquals("Unable to find connector with id: non-existent-id",
exception.getMessage());
+ }
+
+ @Test
+ void testProxiedConnectorEnforcesReadAuthorization() {
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+ final TestConnector proxy = webContext.connector();
+
+ final String result = proxy.readData();
+
+ assertEquals("read-result", result);
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testProxiedConnectorEnforcesWriteAuthorization() {
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+ final TestConnector proxy = webContext.connector();
+
+ proxy.writeData("test-value");
+
+ assertEquals("test-value", lastWrittenValue);
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testProxiedConnectorBlocksUnannotatedMethods() {
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+ final TestConnector proxy = webContext.connector();
+
+ assertThrows(IllegalStateException.class, proxy::unannotatedMethod);
+ }
+
+ @Test
+ void testProxiedConnectorPropagatesAuthorizationFailure() {
+ doThrow(new AccessDeniedException("Access denied"))
+ .when(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+
+ final ConnectorWebContext<TestConnector> webContext =
context.getConnectorWebContext(CONNECTOR_ID);
+ final TestConnector proxy = webContext.connector();
+
+ assertThrows(AccessDeniedException.class, proxy::readData);
+ }
+
+ /**
+ * Test interface representing a Connector with annotated methods.
+ * This interface is used with Mockito's extraInterfaces to create a mock
+ * that implements both this interface and Connector.
+ */
+ public interface TestConnector {
+
+ @ConnectorWebMethod(AccessType.READ)
+ String readData();
+
+ @ConnectorWebMethod(AccessType.WRITE)
+ Void writeData(String value);
+
+ void unannotatedMethod();
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandlerTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandlerTest.java
new file mode 100644
index 0000000000..e12b7366f9
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingConnectorInvocationHandlerTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizationRequest;
+import org.apache.nifi.authorization.AuthorizationResult;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.web.ConnectorWebMethod;
+import org.apache.nifi.web.ConnectorWebMethod.AccessType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.lang.reflect.Proxy;
+import java.util.List;
+
+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.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+public class AuthorizingConnectorInvocationHandlerTest {
+
+ private static final String CONNECTOR_ID = "test-connector-id";
+ private static final String USER_IDENTITY = "test-user";
+
+ @Mock
+ private Authorizer authorizer;
+
+ @Mock
+ private AuthorizableLookup authorizableLookup;
+
+ @Mock
+ private Authorizable connectorAuthorizable;
+
+ @Mock
+ private SecurityContext securityContext;
+
+ @Mock
+ private Authentication authentication;
+
+ private TestConnectorImpl connectorImpl;
+
+ @BeforeEach
+ void setUp() {
+ connectorImpl = new TestConnectorImpl();
+
lenient().when(authorizableLookup.getConnector(CONNECTOR_ID)).thenReturn(connectorAuthorizable);
+
+ final NiFiUser user = new
StandardNiFiUser.Builder().identity(USER_IDENTITY).build();
+ final NiFiUserDetails userDetails = new NiFiUserDetails(user);
+ lenient().when(authentication.getPrincipal()).thenReturn(userDetails);
+
lenient().when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+
+
lenient().when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
+ }
+
+ @AfterEach
+ void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void testReadMethodAuthorizesWithReadAction() {
+ final TestConnector proxy = createProxy();
+
+ final String result = proxy.readData();
+
+ assertEquals("read-result", result);
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.READ, actionCaptor.getValue());
+ }
+
+ @Test
+ void testWriteMethodAuthorizesWithWriteAction() {
+ final TestConnector proxy = createProxy();
+
+ proxy.writeData("test-value");
+
+ assertEquals("test-value", connectorImpl.getLastWrittenValue());
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.WRITE, actionCaptor.getValue());
+ }
+
+ @Test
+ void testMethodWithoutAnnotationThrowsException() {
+ final TestConnector proxy = createProxy();
+
+ final IllegalStateException exception =
assertThrows(IllegalStateException.class, proxy::unannotatedMethod);
+ assertEquals(String.format("Method [unannotatedMethod] on connector
[%s] is not annotated with "
+ + "@ConnectorWebMethod and cannot be invoked through the
Connector Web Context", CONNECTOR_ID), exception.getMessage());
+ }
+
+ @Test
+ void testAuthorizationFailurePropagatesAccessDeniedException() {
+ doThrow(new AccessDeniedException("Access denied for testing"))
+ .when(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+
+ final TestConnector proxy = createProxy();
+
+ assertThrows(AccessDeniedException.class, proxy::readData);
+ }
+
+ @Test
+ void testMethodArgumentsPassedCorrectly() {
+ final TestConnector proxy = createProxy();
+
+ final int result = proxy.processItems(List.of("a", "b", "c"), 10);
+
+ assertEquals(13, result);
+ }
+
+ @Test
+ void testDelegateExceptionUnwrapped() {
+ final TestConnector proxy = createProxy();
+
+ final RuntimeException exception =
assertThrows(RuntimeException.class, proxy::throwingMethod);
+ assertEquals("Intentional test exception", exception.getMessage());
+ }
+
+ @Test
+ void testReadMethodWithReturnValue() {
+ final TestConnector proxy = createProxy();
+
+ final List<String> result = proxy.getItems();
+
+ assertEquals(List.of("item1", "item2", "item3"), result);
+ }
+
+ private TestConnector createProxy() {
+ final AuthorizingConnectorInvocationHandler<TestConnector> handler =
new AuthorizingConnectorInvocationHandler<>(
+ connectorImpl, CONNECTOR_ID, authorizer, authorizableLookup);
+
+ return (TestConnector) Proxy.newProxyInstance(
+ TestConnector.class.getClassLoader(),
+ new Class<?>[]{TestConnector.class},
+ handler);
+ }
+
+ /**
+ * Test interface representing a Connector with annotated methods.
+ */
+ public interface TestConnector {
+
+ @ConnectorWebMethod(AccessType.READ)
+ String readData();
+
+ @ConnectorWebMethod(AccessType.WRITE)
+ void writeData(String value);
+
+ @ConnectorWebMethod(AccessType.READ)
+ List<String> getItems();
+
+ @ConnectorWebMethod(AccessType.WRITE)
+ int processItems(List<String> items, int multiplier);
+
+ @ConnectorWebMethod(AccessType.READ)
+ void throwingMethod();
+
+ void unannotatedMethod();
+ }
+
+ /**
+ * Test implementation of the TestConnector interface.
+ */
+ public static class TestConnectorImpl implements TestConnector {
+
+ private String lastWrittenValue;
+
+ @Override
+ public String readData() {
+ return "read-result";
+ }
+
+ @Override
+ public void writeData(final String value) {
+ this.lastWrittenValue = value;
+ }
+
+ @Override
+ public List<String> getItems() {
+ return List.of("item1", "item2", "item3");
+ }
+
+ @Override
+ public int processItems(final List<String> items, final int
multiplier) {
+ return items.size() + multiplier;
+ }
+
+ @Override
+ public void throwingMethod() {
+ throw new RuntimeException("Intentional test exception");
+ }
+
+ @Override
+ public void unannotatedMethod() {
+ // This method is intentionally not annotated
+ }
+
+ public String getLastWrittenValue() {
+ return lastWrittenValue;
+ }
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContextTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContextTest.java
new file mode 100644
index 0000000000..ddc4f277ba
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingFlowContextTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.authorization.AccessDeniedException;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizationRequest;
+import org.apache.nifi.authorization.AuthorizationResult;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.FlowContextType;
+import org.apache.nifi.components.connector.components.ParameterContextFacade;
+import org.apache.nifi.components.connector.components.ProcessGroupFacade;
+import org.apache.nifi.flow.Bundle;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class AuthorizingFlowContextTest {
+
+ private static final String CONNECTOR_ID = "test-connector-id";
+ private static final String USER_IDENTITY = "test-user";
+
+ @Mock
+ private FlowContext delegate;
+
+ @Mock
+ private Authorizer authorizer;
+
+ @Mock
+ private AuthorizableLookup authorizableLookup;
+
+ @Mock
+ private Authorizable connectorAuthorizable;
+
+ @Mock
+ private ProcessGroupFacade processGroupFacade;
+
+ @Mock
+ private ParameterContextFacade parameterContextFacade;
+
+ @Mock
+ private ConnectorConfigurationContext configurationContext;
+
+ @Mock
+ private SecurityContext securityContext;
+
+ @Mock
+ private Authentication authentication;
+
+ private AuthorizingFlowContext authorizingFlowContext;
+
+ @BeforeEach
+ void setUp() {
+
when(authorizableLookup.getConnector(CONNECTOR_ID)).thenReturn(connectorAuthorizable);
+
lenient().when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
+
+ final NiFiUser user = new
StandardNiFiUser.Builder().identity(USER_IDENTITY).build();
+ final NiFiUserDetails userDetails = new NiFiUserDetails(user);
+ when(authentication.getPrincipal()).thenReturn(userDetails);
+ when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+
+ final ConnectorAuthorizationContext authContext = new
ConnectorAuthorizationContext(CONNECTOR_ID, authorizer, authorizableLookup);
+ authorizingFlowContext = new AuthorizingFlowContext(delegate,
authContext);
+ }
+
+ @AfterEach
+ void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void testGetRootGroupAuthorizesReadAndReturnsWrappedFacade() {
+ when(delegate.getRootGroup()).thenReturn(processGroupFacade);
+
+ final ProcessGroupFacade result =
authorizingFlowContext.getRootGroup();
+
+ assertNotNull(result);
+ assertTrue(result instanceof AuthorizingProcessGroupFacade);
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testGetParameterContextAuthorizesReadAndReturnsWrappedFacade() {
+
when(delegate.getParameterContext()).thenReturn(parameterContextFacade);
+
+ final ParameterContextFacade result =
authorizingFlowContext.getParameterContext();
+
+ assertNotNull(result);
+ assertTrue(result instanceof AuthorizingParameterContextFacade);
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testGetConfigurationContextAuthorizesRead() {
+
when(delegate.getConfigurationContext()).thenReturn(configurationContext);
+
+ final ConnectorConfigurationContext result =
authorizingFlowContext.getConfigurationContext();
+
+ assertNotNull(result);
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testGetTypeAuthorizesRead() {
+ when(delegate.getType()).thenReturn(FlowContextType.WORKING);
+
+ authorizingFlowContext.getType();
+
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testGetBundleAuthorizesRead() {
+ when(delegate.getBundle()).thenReturn(new Bundle("group", "artifact",
"version"));
+
+ authorizingFlowContext.getBundle();
+
+ verify(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+ }
+
+ @Test
+ void testAuthorizationFailurePropagates() {
+ doThrow(new AccessDeniedException("Access denied"))
+ .when(connectorAuthorizable).authorize(any(Authorizer.class),
any(RequestAction.class), any(NiFiUser.class));
+
+ assertThrows(AccessDeniedException.class, () ->
authorizingFlowContext.getRootGroup());
+ }
+}
+
diff --git
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacadeTest.java
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacadeTest.java
new file mode 100644
index 0000000000..6fe3cdfeb3
--- /dev/null
+++
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/connector/authorization/AuthorizingParameterContextFacadeTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.web.connector.authorization;
+
+import org.apache.nifi.asset.Asset;
+import org.apache.nifi.authorization.AuthorizableLookup;
+import org.apache.nifi.authorization.AuthorizationRequest;
+import org.apache.nifi.authorization.AuthorizationResult;
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.authorization.user.StandardNiFiUser;
+import org.apache.nifi.components.connector.components.ParameterContextFacade;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class AuthorizingParameterContextFacadeTest {
+
+ private static final String CONNECTOR_ID = "test-connector-id";
+ private static final String USER_IDENTITY = "test-user";
+
+ @Mock
+ private ParameterContextFacade delegate;
+
+ @Mock
+ private Authorizer authorizer;
+
+ @Mock
+ private AuthorizableLookup authorizableLookup;
+
+ @Mock
+ private Authorizable connectorAuthorizable;
+
+ @Mock
+ private SecurityContext securityContext;
+
+ @Mock
+ private Authentication authentication;
+
+ @Mock
+ private Asset asset;
+
+ private AuthorizingParameterContextFacade authorizingFacade;
+
+ @BeforeEach
+ void setUp() {
+
when(authorizableLookup.getConnector(CONNECTOR_ID)).thenReturn(connectorAuthorizable);
+
lenient().when(authorizer.authorize(any(AuthorizationRequest.class))).thenReturn(AuthorizationResult.approved());
+
+ final NiFiUser user = new
StandardNiFiUser.Builder().identity(USER_IDENTITY).build();
+ final NiFiUserDetails userDetails = new NiFiUserDetails(user);
+ when(authentication.getPrincipal()).thenReturn(userDetails);
+ when(securityContext.getAuthentication()).thenReturn(authentication);
+ SecurityContextHolder.setContext(securityContext);
+
+ final ConnectorAuthorizationContext authContext = new
ConnectorAuthorizationContext(CONNECTOR_ID, authorizer, authorizableLookup);
+ authorizingFacade = new AuthorizingParameterContextFacade(delegate,
authContext);
+ }
+
+ @AfterEach
+ void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ void testGetValueAuthorizesWithReadAction() {
+ when(delegate.getValue("param1")).thenReturn("value1");
+
+ final String result = authorizingFacade.getValue("param1");
+
+ assertEquals("value1", result);
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.READ, actionCaptor.getValue());
+ }
+
+ @Test
+ void testGetDefinedParameterNamesAuthorizesWithReadAction() {
+ when(delegate.getDefinedParameterNames()).thenReturn(Set.of("param1",
"param2"));
+
+ final Set<String> result =
authorizingFacade.getDefinedParameterNames();
+
+ assertEquals(Set.of("param1", "param2"), result);
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.READ, actionCaptor.getValue());
+ }
+
+ @Test
+ void testIsSensitiveAuthorizesWithReadAction() {
+ when(delegate.isSensitive("param1")).thenReturn(true);
+
+ final boolean result = authorizingFacade.isSensitive("param1");
+
+ assertTrue(result);
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.READ, actionCaptor.getValue());
+ }
+
+ @Test
+ void testUpdateParametersAuthorizesWithWriteAction() {
+ authorizingFacade.updateParameters(Collections.emptyList());
+
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.WRITE, actionCaptor.getValue());
+ verify(delegate).updateParameters(Collections.emptyList());
+ }
+
+ @Test
+ void testCreateAssetAuthorizesWithWriteAction() throws IOException {
+ final ByteArrayInputStream inputStream = new ByteArrayInputStream(new
byte[0]);
+ when(delegate.createAsset(inputStream)).thenReturn(asset);
+
+ final Asset result = authorizingFacade.createAsset(inputStream);
+
+ assertEquals(asset, result);
+ final ArgumentCaptor<RequestAction> actionCaptor =
ArgumentCaptor.forClass(RequestAction.class);
+ verify(connectorAuthorizable).authorize(eq(authorizer),
actionCaptor.capture(), any(NiFiUser.class));
+ assertEquals(RequestAction.WRITE, actionCaptor.getValue());
+ }
+}
+