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

exceptionfactory 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 9626e31b7d8 NIFI-15629 Add ConnectorRequestContext (#10924)
9626e31b7d8 is described below

commit 9626e31b7d81bbf8e11032b5ca6e6d949d6317f4
Author: Kevin Doran <[email protected]>
AuthorDate: Fri Feb 27 16:46:43 2026 -0500

    NIFI-15629 Add ConnectorRequestContext (#10924)
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../connector/ConnectorConfigurationProvider.java  |  21 +++
 .../connector/ConnectorRequestContext.java         |  88 ++++++++++
 .../connector/ConnectorRequestContextHolder.java   |  64 ++++++++
 .../connector/StandardConnectorRequestContext.java |  84 ++++++++++
 .../ConnectorRequestContextHolderTest.java         |  69 ++++++++
 .../connector/StandardConnectorRepository.java     |   6 +
 .../web/filter/ConnectorRequestContextFilter.java  |  89 +++++++++++
 .../nifi-web-api/src/main/webapp/WEB-INF/web.xml   |   8 +
 .../filter/ConnectorRequestContextFilterTest.java  | 178 +++++++++++++++++++++
 9 files changed, 607 insertions(+)

diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
index c61b80a8951..4d2ff3ecc88 100644
--- 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorConfigurationProvider.java
@@ -122,6 +122,27 @@ public interface ConnectorConfigurationProvider {
      */
     void deleteAsset(String connectorId, String nifiUuid);
 
+    /**
+     * Called when a connector update is requested (e.g., applying a committed 
configuration change)
+     * to determine whether the framework should proceed with the standard 
internal update process
+     * (stopping, re-configuring, and restarting the connector).
+     *
+     * <p>Returning {@code true} (the default) indicates the framework should 
proceed normally.</p>
+     *
+     * <p>Returning {@code false} indicates the framework should skip the 
update and return
+     * immediately -- this is not a failure; the provider may have handled the 
update externally
+     * by doing some bookkeeping logic and the provider may re-trigger the 
update process by starting
+     * a new request to the nifi framework once it is ready to proceed. If the 
provider wants to fail
+     * the request, it should throw a runtime exception instead.</p>
+     *
+     * @param connectorId the identifier of the connector to update
+     * @return {@code true} if the framework should proceed with the standard 
update process,
+     *         {@code false} if the framework should skip the update (no-op)
+     */
+    default boolean shouldApplyUpdate(final String connectorId) {
+        return true;
+    }
+
     /**
      * Ensures that local asset binaries are up to date with the external 
store. For each asset
      * tracked in the provider's local state, this method compares the 
external store's current
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContext.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContext.java
new file mode 100644
index 00000000000..a722abac257
--- /dev/null
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContext.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.components.connector;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Provides request-scoped context from an HTTP request to a {@link 
ConnectorConfigurationProvider}.
+ *
+ * <p>When a connector operation is triggered by an HTTP request (e.g., from 
the Runtime UI),
+ * the framework populates this context with the authenticated NiFi user and 
all HTTP headers
+ * from the original request. Provider implementations can use this to make 
decisions based on
+ * who is making the request and to extract forwarded credentials (e.g., OAuth 
tokens relayed
+ * by a gateway).</p>
+ *
+ * <p>This context is available via {@link 
ConnectorRequestContextHolder#getContext()} on the
+ * request thread. When no HTTP request is in scope (e.g., background 
operations), the holder
+ * returns {@code null}.</p>
+ */
+public interface ConnectorRequestContext {
+
+    /**
+     * Returns the authenticated NiFi user who initiated the request. For 
proxied requests
+     * (e.g., through a gateway), this is the end-user with the proxy chain 
accessible
+     * via {@link NiFiUser#getChain()}.
+     *
+     * @return the authenticated NiFi user, or {@code null} if not available
+     */
+    NiFiUser getAuthenticatedUser();
+
+    /**
+     * Returns all HTTP headers from the original request as an immutable, 
case-insensitive
+     * multi-valued map. Each header name maps to an unmodifiable list of its 
values.
+     *
+     * <p>Provider implementations should read the specific headers they need 
and ignore
+     * the rest. This allows new headers to be forwarded by a gateway without 
requiring
+     * changes to the NiFi framework.</p>
+     *
+     * @return an immutable map of header names to their values
+     */
+    Map<String, List<String>> getRequestHeaders();
+
+    /**
+     * Returns whether the request contains a header with the given name.
+     * Header name matching is case-insensitive per the HTTP specification.
+     *
+     * @param headerName the header name to check
+     * @return {@code true} if the header is present, {@code false} otherwise
+     */
+    boolean hasRequestHeader(String headerName);
+
+    /**
+     * Returns all values for the given header name, or an empty list if the 
header is not present.
+     * Header name matching is case-insensitive per the HTTP specification.
+     *
+     * @param headerName the header name to look up
+     * @return an unmodifiable list of header values, or an empty list if not 
present
+     */
+    List<String> getRequestHeaderValues(String headerName);
+
+    /**
+     * Returns the first value for the given header name, or {@code null} if 
the header is
+     * not present or has no values. Header name matching is case-insensitive 
per the HTTP
+     * specification.
+     *
+     * @param headerName the header name to look up
+     * @return the first header value, or {@code null} if not present
+     */
+    String getFirstRequestHeaderValue(String headerName);
+}
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContextHolder.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContextHolder.java
new file mode 100644
index 00000000000..db46ec53a30
--- /dev/null
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/ConnectorRequestContextHolder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.components.connector;
+
+/**
+ * Thread-local holder for {@link ConnectorRequestContext}. This class resides 
in
+ * {@code nifi-framework-api} so that it is on the shared classloader, 
accessible
+ * from both the NiFi web layer (which sets the context) and NAR-loaded
+ * {@link ConnectorConfigurationProvider} implementations (which read it).
+ *
+ * <p>The context is set by the framework's request filter before connector 
operations
+ * and cleared after the request completes. Provider implementations should 
access the
+ * context via {@link #getContext()} and must not assume it is always present 
-- it will
+ * be {@code null} for operations not triggered by an HTTP request (e.g., 
background tasks).</p>
+ */
+public final class ConnectorRequestContextHolder {
+
+    private static final ThreadLocal<ConnectorRequestContext> CONTEXT = new 
ThreadLocal<>();
+
+    private ConnectorRequestContextHolder() {
+    }
+
+    /**
+     * Returns the {@link ConnectorRequestContext} for the current thread, or 
{@code null}
+     * if no context has been set (e.g., for background or non-HTTP-request 
operations).
+     *
+     * @return the current request context, or {@code null}
+     */
+    public static ConnectorRequestContext getContext() {
+        return CONTEXT.get();
+    }
+
+    /**
+     * Sets the {@link ConnectorRequestContext} for the current thread.
+     *
+     * @param context the request context to set
+     */
+    public static void setContext(final ConnectorRequestContext context) {
+        CONTEXT.set(context);
+    }
+
+    /**
+     * Clears the {@link ConnectorRequestContext} from the current thread.
+     * This must be called after request processing completes to prevent 
memory leaks.
+     */
+    public static void clearContext() {
+        CONTEXT.remove();
+    }
+}
diff --git 
a/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/StandardConnectorRequestContext.java
 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/StandardConnectorRequestContext.java
new file mode 100644
index 00000000000..97a7e006019
--- /dev/null
+++ 
b/nifi-framework-api/src/main/java/org/apache/nifi/components/connector/StandardConnectorRequestContext.java
@@ -0,0 +1,84 @@
+/*
+ * 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.components.connector;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Standard implementation of {@link ConnectorRequestContext} that stores an 
authenticated
+ * {@link NiFiUser} and HTTP headers in a case-insensitive map.
+ *
+ * <p>Header name lookups via {@link #hasRequestHeader}, {@link 
#getRequestHeaderValues}, and
+ * {@link #getFirstRequestHeaderValue} are case-insensitive per the HTTP 
specification.
+ * The backing map uses {@link String#CASE_INSENSITIVE_ORDER} to guarantee 
this.</p>
+ */
+public class StandardConnectorRequestContext implements 
ConnectorRequestContext {
+
+    private final NiFiUser niFiUser;
+    private final Map<String, List<String>> requestHeaders;
+
+    /**
+     * Creates a new context with the given user and headers. The provided 
headers map is
+     * copied into a case-insensitive map; the original map is not retained.
+     *
+     * @param niFiUser the authenticated NiFi user, or {@code null} if not 
available
+     * @param requestHeaders the HTTP headers from the request; may be {@code 
null} or empty
+     */
+    public StandardConnectorRequestContext(final NiFiUser niFiUser, final 
Map<String, List<String>> requestHeaders) {
+        this.niFiUser = niFiUser;
+        if (requestHeaders == null || requestHeaders.isEmpty()) {
+            this.requestHeaders = Map.of();
+        } else {
+            final Map<String, List<String>> caseInsensitive = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+            caseInsensitive.putAll(requestHeaders);
+            this.requestHeaders = Collections.unmodifiableMap(caseInsensitive);
+        }
+    }
+
+    @Override
+    public NiFiUser getAuthenticatedUser() {
+        return niFiUser;
+    }
+
+    @Override
+    public Map<String, List<String>> getRequestHeaders() {
+        return requestHeaders;
+    }
+
+    @Override
+    public boolean hasRequestHeader(final String headerName) {
+        return requestHeaders.containsKey(headerName);
+    }
+
+    @Override
+    public List<String> getRequestHeaderValues(final String headerName) {
+        final List<String> values = requestHeaders.get(headerName);
+        return values != null ? values : List.of();
+    }
+
+    @Override
+    public String getFirstRequestHeaderValue(final String headerName) {
+        final List<String> values = getRequestHeaderValues(headerName);
+        return values.isEmpty() ? null : values.getFirst();
+    }
+}
diff --git 
a/nifi-framework-api/src/test/java/org/apache/nifi/components/connector/ConnectorRequestContextHolderTest.java
 
b/nifi-framework-api/src/test/java/org/apache/nifi/components/connector/ConnectorRequestContextHolderTest.java
new file mode 100644
index 00000000000..ce9cba3eb67
--- /dev/null
+++ 
b/nifi-framework-api/src/test/java/org/apache/nifi/components/connector/ConnectorRequestContextHolderTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.components.connector;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class ConnectorRequestContextHolderTest {
+
+    @AfterEach
+    void clearContext() {
+        ConnectorRequestContextHolder.clearContext();
+    }
+
+    @Test
+    void testGetContextReturnsNullByDefault() {
+        assertNull(ConnectorRequestContextHolder.getContext());
+    }
+
+    @Test
+    void testSetAndGetContext() {
+        final ConnectorRequestContext context = new 
StandardConnectorRequestContext(null, Map.of());
+        ConnectorRequestContextHolder.setContext(context);
+
+        assertEquals(context, ConnectorRequestContextHolder.getContext());
+    }
+
+    @Test
+    void testClearContextRemovesContext() {
+        final ConnectorRequestContext context = new 
StandardConnectorRequestContext(null, Map.of());
+        ConnectorRequestContextHolder.setContext(context);
+        ConnectorRequestContextHolder.clearContext();
+
+        assertNull(ConnectorRequestContextHolder.getContext());
+    }
+
+    @Test
+    void testContextIsThreadLocal() throws Exception {
+        final ConnectorRequestContext context = new 
StandardConnectorRequestContext(null, Map.of());
+        ConnectorRequestContextHolder.setContext(context);
+
+        final ConnectorRequestContext[] otherThreadContext = new 
ConnectorRequestContext[1];
+        final Thread thread = new Thread(() -> otherThreadContext[0] = 
ConnectorRequestContextHolder.getContext());
+        thread.start();
+        thread.join();
+
+        assertNull(otherThreadContext[0]);
+        assertEquals(context, ConnectorRequestContextHolder.getContext());
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
index 4abe276c566..2df018c6382 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/StandardConnectorRepository.java
@@ -171,6 +171,12 @@ public class StandardConnectorRepository implements 
ConnectorRepository {
     @Override
     public void applyUpdate(final ConnectorNode connector, final 
ConnectorUpdateContext context) throws FlowUpdateException {
         logger.debug("Applying update to {}", connector);
+
+        if (configurationProvider != null && 
!configurationProvider.shouldApplyUpdate(connector.getIdentifier())) {
+            logger.info("ConnectorConfigurationProvider indicated framework 
should not apply update for {}; skipping framework update process", connector);
+            return;
+        }
+
         final ConnectorState initialDesiredState = connector.getDesiredState();
         logger.info("Applying update to Connector {}", connector);
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/filter/ConnectorRequestContextFilter.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/filter/ConnectorRequestContextFilter.java
new file mode 100644
index 00000000000..75d76d0a8c2
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/filter/ConnectorRequestContextFilter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.filter;
+
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserUtils;
+import org.apache.nifi.components.connector.ConnectorRequestContext;
+import org.apache.nifi.components.connector.ConnectorRequestContextHolder;
+import org.apache.nifi.components.connector.StandardConnectorRequestContext;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Servlet filter that populates the {@link ConnectorRequestContextHolder} 
thread-local
+ * with the current HTTP request headers and authenticated {@link NiFiUser}.
+ *
+ * <p>This filter must be registered after the Spring Security filter chain so 
that the
+ * authenticated {@link NiFiUser} is available via the security context. The 
context is
+ * always cleared in the {@code finally} block to prevent thread-local memory 
leaks.</p>
+ */
+public class ConnectorRequestContextFilter implements Filter {
+
+    @Override
+    public void doFilter(final ServletRequest request, final ServletResponse 
response, final FilterChain chain)
+            throws IOException, ServletException {
+        try {
+            if (request instanceof HttpServletRequest httpServletRequest) {
+                final ConnectorRequestContext context = 
createContext(httpServletRequest);
+                ConnectorRequestContextHolder.setContext(context);
+            }
+            chain.doFilter(request, response);
+        } finally {
+            ConnectorRequestContextHolder.clearContext();
+        }
+    }
+
+    private ConnectorRequestContext createContext(final HttpServletRequest 
request) {
+        final NiFiUser nifiUser = NiFiUserUtils.getNiFiUser();
+        final Map<String, List<String>> headers = captureHeaders(request);
+        return new StandardConnectorRequestContext(nifiUser, headers);
+    }
+
+    private Map<String, List<String>> captureHeaders(final HttpServletRequest 
request) {
+        final Map<String, List<String>> headers = new HashMap<>();
+        final Enumeration<String> headerNames = request.getHeaderNames();
+        if (headerNames != null) {
+            while (headerNames.hasMoreElements()) {
+                final String name = headerNames.nextElement();
+                final List<String> values = 
Collections.list(request.getHeaders(name));
+                headers.put(name, Collections.unmodifiableList(values));
+            }
+        }
+        return headers;
+    }
+
+    @Override
+    public void init(final FilterConfig filterConfig) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/webapp/WEB-INF/web.xml
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/webapp/WEB-INF/web.xml
index af90238f98f..1025a301d54 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/webapp/WEB-INF/web.xml
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/webapp/WEB-INF/web.xml
@@ -61,4 +61,12 @@
         <filter-name>springSecurityFilterChain</filter-name>
         <url-pattern>/*</url-pattern>
     </filter-mapping>
+    <filter>
+        <filter-name>connectorRequestContextFilter</filter-name>
+        
<filter-class>org.apache.nifi.web.filter.ConnectorRequestContextFilter</filter-class>
+    </filter>
+    <filter-mapping>
+        <filter-name>connectorRequestContextFilter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
 </web-app>
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/filter/ConnectorRequestContextFilterTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/filter/ConnectorRequestContextFilterTest.java
new file mode 100644
index 00000000000..1ceef675efb
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/filter/ConnectorRequestContextFilterTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletResponse;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.authorization.user.NiFiUserDetails;
+import org.apache.nifi.components.connector.ConnectorRequestContext;
+import org.apache.nifi.components.connector.ConnectorRequestContextHolder;
+import org.junit.jupiter.api.AfterEach;
+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.mockito.quality.Strictness;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+@ExtendWith(MockitoExtension.class)
+class ConnectorRequestContextFilterTest {
+
+    private static final String TEST_USER = "[email protected]";
+    private static final String TOKEN_HEADER = "API-Key";
+    private static final String TOKEN_VALUE = "test-api-key-123";
+    private static final String ROLE_HEADER = "API-Role";
+    private static final String ROLE_VALUE = "SERVICE_ROLE";
+
+    @Mock
+    private FilterChain filterChain;
+
+    @Mock
+    private ServletResponse response;
+
+    @AfterEach
+    void cleanup() {
+        ConnectorRequestContextHolder.clearContext();
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    void testContextIsSetDuringFilterChain() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader(TOKEN_HEADER, TOKEN_VALUE);
+        request.addHeader(ROLE_HEADER, ROLE_VALUE);
+        setUpSecurityContext();
+
+        final ConnectorRequestContext[] capturedContext = new 
ConnectorRequestContext[1];
+        doAnswer(invocation -> {
+            capturedContext[0] = ConnectorRequestContextHolder.getContext();
+            return null;
+        }).when(filterChain).doFilter(any(), any());
+
+        final ConnectorRequestContextFilter filter = new 
ConnectorRequestContextFilter();
+        filter.doFilter(request, response, filterChain);
+
+        assertNotNull(capturedContext[0]);
+        assertNotNull(capturedContext[0].getAuthenticatedUser());
+        assertEquals(TEST_USER, 
capturedContext[0].getAuthenticatedUser().getIdentity());
+
+        assertTrue(capturedContext[0].hasRequestHeader(TOKEN_HEADER));
+        assertTrue(capturedContext[0].hasRequestHeader(ROLE_HEADER));
+        assertEquals(TOKEN_VALUE, 
capturedContext[0].getFirstRequestHeaderValue(TOKEN_HEADER));
+        assertEquals(ROLE_VALUE, 
capturedContext[0].getFirstRequestHeaderValue(ROLE_HEADER));
+        assertEquals(List.of(TOKEN_VALUE), 
capturedContext[0].getRequestHeaderValues(TOKEN_HEADER));
+        assertEquals(List.of(ROLE_VALUE), 
capturedContext[0].getRequestHeaderValues(ROLE_HEADER));
+    }
+
+    @Test
+    void testContextIsClearedAfterFilterChain() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader(TOKEN_HEADER, TOKEN_VALUE);
+        setUpSecurityContext();
+
+        final ConnectorRequestContextFilter filter = new 
ConnectorRequestContextFilter();
+        filter.doFilter(request, response, filterChain);
+
+        assertNull(ConnectorRequestContextHolder.getContext());
+    }
+
+    @Test
+    void testContextIsClearedAfterFilterChainException() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        setUpSecurityContext();
+
+        doAnswer(invocation -> {
+            throw new RuntimeException("simulated error");
+        }).when(filterChain).doFilter(any(), any());
+
+        final ConnectorRequestContextFilter filter = new 
ConnectorRequestContextFilter();
+        try {
+            filter.doFilter(request, response, filterChain);
+        } catch (final RuntimeException ignored) {
+            // expected
+        }
+
+        assertNull(ConnectorRequestContextHolder.getContext());
+    }
+
+    @Test
+    void testContextWithNoAuthenticatedUser() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader(TOKEN_HEADER, TOKEN_VALUE);
+
+        final ConnectorRequestContext[] capturedContext = new 
ConnectorRequestContext[1];
+        doAnswer(invocation -> {
+            capturedContext[0] = ConnectorRequestContextHolder.getContext();
+            return null;
+        }).when(filterChain).doFilter(any(), any());
+
+        final ConnectorRequestContextFilter filter = new 
ConnectorRequestContextFilter();
+        filter.doFilter(request, response, filterChain);
+
+        assertNotNull(capturedContext[0]);
+        assertNull(capturedContext[0].getAuthenticatedUser());
+        assertEquals(TOKEN_VALUE, 
capturedContext[0].getFirstRequestHeaderValue(TOKEN_HEADER));
+    }
+
+    @Test
+    void testHeaderMapIsCaseInsensitive() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader(TOKEN_HEADER, TOKEN_VALUE);
+
+        final ConnectorRequestContext[] capturedContext = new 
ConnectorRequestContext[1];
+        doAnswer(invocation -> {
+            capturedContext[0] = ConnectorRequestContextHolder.getContext();
+            return null;
+        }).when(filterChain).doFilter(any(), any());
+
+        final ConnectorRequestContextFilter filter = new 
ConnectorRequestContextFilter();
+        filter.doFilter(request, response, filterChain);
+
+        assertNotNull(capturedContext[0]);
+        assertEquals(TOKEN_VALUE, 
capturedContext[0].getFirstRequestHeaderValue(TOKEN_HEADER.toLowerCase()));
+        assertEquals(TOKEN_VALUE, 
capturedContext[0].getFirstRequestHeaderValue(TOKEN_HEADER.toUpperCase()));
+        
assertTrue(capturedContext[0].hasRequestHeader(TOKEN_HEADER.toLowerCase()));
+        
assertTrue(capturedContext[0].hasRequestHeader(TOKEN_HEADER.toUpperCase()));
+        
assertFalse(capturedContext[0].hasRequestHeader("Non-Existent-Header"));
+        assertEquals(List.of(), 
capturedContext[0].getRequestHeaderValues("Non-Existent-Header"));
+        
assertNull(capturedContext[0].getFirstRequestHeaderValue("Non-Existent-Header"));
+    }
+
+    private void setUpSecurityContext() {
+        final NiFiUser user = mock(NiFiUser.class, 
withSettings().strictness(Strictness.LENIENT));
+        when(user.getIdentity()).thenReturn(TEST_USER);
+        final NiFiUserDetails userDetails = new NiFiUserDetails(user);
+        final TestingAuthenticationToken authentication = new 
TestingAuthenticationToken(userDetails, "");
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+    }
+}

Reply via email to