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);
}
}