This is an automated email from the ASF dual-hosted git repository. markap14 pushed a commit to branch NIFI-15258 in repository https://gitbox.apache.org/repos/asf/nifi.git
commit 7417597727e616d5ba89e870c54f96c50c2571e8 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 9130a348f7..a1472b8c6d 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; @@ -7374,6 +7375,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()); + } +} +
