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

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 941d42ae6 KNOX-3100 - RemoteAuthProvider to accept multiple group 
headers (#997)
941d42ae6 is described below

commit 941d42ae65e941d7e392b44107b9908c750cdf16
Author: lmccay <[email protected]>
AuthorDate: Wed Feb 26 17:48:57 2025 -0500

    KNOX-3100 - RemoteAuthProvider to accept multiple group headers (#997)
    
    * KNOX-3100 - RemoteAuthProvider to accept multiple group headers
---
 .../knox/gateway/filter/RemoteAuthFilter.java      | 53 +++++++++++++-----
 .../knox/gateway/filter/RemoteAuthFilterTest.java  | 63 +++++++++++++++++++---
 2 files changed, 96 insertions(+), 20 deletions(-)

diff --git 
a/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
 
b/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
index 7f90adef2..563e86978 100755
--- 
a/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
+++ 
b/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
@@ -70,13 +70,14 @@ public class RemoteAuthFilter implements Filter {
   private static final String CONFIG_USER_HEADER = "remote.auth.user.header";
   private static final String CONFIG_GROUP_HEADER = "remote.auth.group.header";
   private static final String DEFAULT_CONFIG_USER_HEADER = "X-Knox-Actor-ID";
-  private static final String DEFAULT_CONFIG_GROUP_HEADER = 
"X-Knox-Actor-Groups-1";
+  private static final String DEFAULT_CONFIG_GROUP_HEADER = 
"X-Knox-Actor-Groups-*";
+  private static final String WILDCARD = "*";
 
   private String remoteAuthUrl;
   private List<String> includeHeaders;
   private String cacheKeyHeader;
   private String userHeader;
-  private String groupHeader;
+  private List<String> groupHeaders;
   /*
   For Testing
    */
@@ -112,10 +113,11 @@ public class RemoteAuthFilter implements Filter {
       userHeader = DEFAULT_CONFIG_USER_HEADER;
     }
 
-    groupHeader = filterConfig.getInitParameter(CONFIG_GROUP_HEADER);
-    if (groupHeader == null || groupHeader.isEmpty()) {
-      userHeader = DEFAULT_CONFIG_GROUP_HEADER;
-
+    String groupHeaderParam = 
filterConfig.getInitParameter(CONFIG_GROUP_HEADER);
+    if (groupHeaderParam == null || groupHeaderParam.isEmpty()) {
+      groupHeaders = Arrays.asList(DEFAULT_CONFIG_GROUP_HEADER);
+    } else {
+      groupHeaders = Arrays.asList(groupHeaderParam.split("\\s*,\\s*"));
     }
   }
 
@@ -154,14 +156,10 @@ public class RemoteAuthFilter implements Filter {
       int responseCode = connection.getResponseCode();
       if (responseCode == HttpURLConnection.HTTP_OK) {
         String principalName = connection.getHeaderField(userHeader);
-        String groupNames = connection.getHeaderField(groupHeader);
         Subject subject = new Subject();
         subject.getPrincipals().add(new PrimaryPrincipal(principalName));
-        // Add groups to the principal if available
-        if(groupNames != null && !groupNames.isEmpty()) {
-          Arrays.stream(groupNames.split(",")).forEach(groupName -> 
subject.getPrincipals()
-                  .add(new GroupPrincipal(groupName)));
-        }
+
+        addGroupPrincipals(subject, connection);
 
         authenticationCache.put(hashCacheKey(cacheKey), subject);
 
@@ -244,6 +242,37 @@ public class RemoteAuthFilter implements Filter {
     }
   }
 
+  private void addGroupPrincipals(Subject subject, HttpURLConnection 
connection) {
+    for (String headerPattern : groupHeaders) {
+      if (headerPattern.endsWith(WILDCARD)) {
+        // Handle wildcard pattern
+        String prefix = headerPattern.substring(0, headerPattern.length() - 1);
+        connection.getHeaderFields().forEach((key, value) -> {
+          if (key != null && key.startsWith(prefix)) {
+            addGroupsFromHeaderValue(subject, value);
+          }
+        });
+      } else {
+        // Handle exact header match
+        String groupNames = connection.getHeaderField(headerPattern);
+        if (groupNames != null && !groupNames.isEmpty()) {
+          addGroupsFromHeaderValue(subject, Arrays.asList(groupNames));
+        }
+      }
+    }
+  }
+
+  private void addGroupsFromHeaderValue(Subject subject, List<String> 
headerValues) {
+    headerValues.forEach(headerValue -> {
+      if (headerValue != null && !headerValue.isEmpty()) {
+        Arrays.stream(headerValue.split(","))
+              .map(String::trim)
+              .filter(group -> !group.isEmpty())
+              .forEach(groupName -> subject.getPrincipals().add(new 
GroupPrincipal(groupName)));
+      }
+    });
+  }
+
   @Override
   public void destroy() {
   }
diff --git 
a/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
 
b/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
index 3d6679a35..09c27aec3 100644
--- 
a/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
+++ 
b/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
@@ -38,8 +38,10 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.security.AccessController;
 import java.security.Principal;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -55,6 +57,9 @@ public class RemoteAuthFilterTest {
     private static final String URL_FAIL = "http://example.com/authfail";;
     public static final String X_AUTHENTICATED_USER = "X-Authenticated-User";
     public static final String X_AUTHENTICATED_GROUP = "X-Authenticated-Group";
+    public static final String X_AUTHENTICATED_GROUP_2 = 
"X-Authenticated-Group-2";
+    public static final String X_CUSTOM_GROUP_1 = "X-Custom-Group-1";
+    public static final String X_CUSTOM_GROUP_2 = "X-Custom-Group-2";
     private RemoteAuthFilter filter;
     private HttpServletRequest requestMock;
     private HttpServletResponse responseMock;
@@ -71,7 +76,8 @@ public class RemoteAuthFilterTest {
         
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.cache.key")).andReturn("Authorization").anyTimes();
         
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.expire.after")).andReturn("5").anyTimes();
         
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.user.header")).andReturn(X_AUTHENTICATED_USER).anyTimes();
-        
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.group.header")).andReturn(X_AUTHENTICATED_GROUP).anyTimes();
+        
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.group.header"))
+               .andReturn(X_AUTHENTICATED_GROUP + "," + 
X_AUTHENTICATED_GROUP_2 + ",X-Custom-Group-*").anyTimes();
 
         EasyMock.replay(filterConfigMock);
 
@@ -193,10 +199,49 @@ public class RemoteAuthFilterTest {
         }
     }
 
+    @Test
+    public void successfulAuthenticationWithMultipleGroups() throws Exception {
+        EasyMock.expect(requestMock.getServletContext()).andReturn(new 
MockServletContext()).anyTimes();
+        
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_VALID_TOKEN).anyTimes();
+        EasyMock.expect(responseMock.getStatus()).andReturn(200).anyTimes();
+        
responseMock.sendError(EasyMock.eq(HttpServletResponse.SC_UNAUTHORIZED), 
EasyMock.anyString());
+        EasyMock.expectLastCall().andThrow(new AssertionError("Authentication 
should be successful, but was not.")).anyTimes();
+
+        EasyMock.replay(requestMock, responseMock);
+
+        try {
+            MockHttpURLConnection mockConn = new MockHttpURLConnection(new 
URL(URL_SUCCESS));
+            // Add groups from multiple headers
+            mockConn.addHeader(X_AUTHENTICATED_GROUP, "admin,engineers");
+            mockConn.addHeader(X_AUTHENTICATED_GROUP_2, "developers");
+            mockConn.addHeader(X_CUSTOM_GROUP_1, "team-a");
+            mockConn.addHeader(X_CUSTOM_GROUP_2, "team-b,team-c");
+            filter.httpURLConnection = mockConn;
+
+            filter.doFilter(requestMock, responseMock, chainMock);
+            assertEquals(responseMock.getStatus(), HttpServletResponse.SC_OK);
+
+            assertTrue("Filter chain should have been called but wasn't", 
chainMock.doFilterCalled);
+
+            Set<GroupPrincipal> groupPrincipals = 
chainMock.subject.getPrincipals(GroupPrincipal.class);
+            assertEquals("Should have all groups from all headers", 6, 
groupPrincipals.size());
+
+            // Verify groups from different headers
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("admin")));
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("engineers")));
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("developers")));
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("team-a")));
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("team-b")));
+            assertTrue(groupPrincipals.stream().anyMatch(p -> 
p.getName().equals("team-c")));
+        } catch (AssertionError e) {
+            assert false : "Authentication failed unexpectedly";
+        }
+    }
+
     public static class MockHttpURLConnection extends HttpURLConnection {
         private final URL url;
         private int responseCode;
-        private final Map<String, String> headers;
+        private final Map<String, List<String>> headers;
 
         public MockHttpURLConnection(URL url) {
             super(url);
@@ -205,8 +250,8 @@ public class RemoteAuthFilterTest {
             this.headers = new HashMap<>();
 
             if (url.toString().equals(URL_SUCCESS)) {
-                headers.put(X_AUTHENTICATED_USER, "lmccay");
-                headers.put(X_AUTHENTICATED_GROUP, "admin,engineers");
+                addHeader(X_AUTHENTICATED_USER, "lmccay");
+                addHeader(X_AUTHENTICATED_GROUP, "admin,engineers");
             }
         }
 
@@ -236,15 +281,17 @@ public class RemoteAuthFilterTest {
 
         @Override
         public String getHeaderField(String name) {
-            return headers.get(name);
+            List<String> values = headers.get(name);
+            return values != null && !values.isEmpty() ? values.get(0) : null;
         }
 
-        public void setResponseCode(int code) {
-            this.responseCode = code;
+        @Override
+        public Map<String, List<String>> getHeaderFields() {
+            return headers;
         }
 
         public void addHeader(String name, String value) {
-            headers.put(name, value);
+            headers.computeIfAbsent(name, k -> new ArrayList<>()).add(value);
         }
     }
 

Reply via email to