Skip to content

Commit 29c606f

Browse files
authored
KNOX-3105 - Add Topology Level Config for Truststore to RemoteAuthProvider (#1001)
* KNOX-3105 - Add Topology Level Config for Truststore to RemoteAuthProvider
1 parent 139f017 commit 29c606f

File tree

4 files changed

+247
-37
lines changed

4 files changed

+247
-37
lines changed

gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java

+52-24
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,24 @@
6363

6464
public class RemoteAuthFilter implements Filter {
6565

66-
private static final String CONFIG_REMOTE_AUTH_URL = "remote.auth.url";
67-
private static final String CONFIG_INCLUDE_HEADERS = "remote.auth.include.headers";
68-
private static final String CONFIG_CACHE_KEY_HEADER = "remote.auth.cache.key";
69-
private static final String CONFIG_EXPIRE_AFTER = "remote.auth.expire.after";
70-
private static final String DEFAULT_CACHE_KEY_HEADER = "Authorization";
71-
private static final String CONFIG_USER_HEADER = "remote.auth.user.header";
72-
private static final String CONFIG_GROUP_HEADER = "remote.auth.group.header";
73-
private static final String DEFAULT_CONFIG_USER_HEADER = "X-Knox-Actor-ID";
74-
private static final String DEFAULT_CONFIG_GROUP_HEADER = "X-Knox-Actor-Groups-*";
75-
private static final String WILDCARD = "*";
66+
static final String REMOTE_AUTH = "remote.auth.";
67+
static final String CONFIG_REMOTE_AUTH_URL = REMOTE_AUTH + "url";
68+
static final String CONFIG_INCLUDE_HEADERS = REMOTE_AUTH + "include.headers";
69+
static final String CONFIG_CACHE_KEY_HEADER = REMOTE_AUTH + "cache.key";
70+
static final String CONFIG_EXPIRE_AFTER = REMOTE_AUTH + "expire.after";
71+
static final String DEFAULT_CACHE_KEY_HEADER = "Authorization";
72+
static final String CONFIG_USER_HEADER = REMOTE_AUTH + "user.header";
73+
static final String CONFIG_GROUP_HEADER = REMOTE_AUTH + "group.header";
74+
static final String DEFAULT_CONFIG_USER_HEADER = "X-Knox-Actor-ID";
75+
static final String DEFAULT_CONFIG_GROUP_HEADER = "X-Knox-Actor-Groups-*";
76+
static final String CONFIG_TRUSTSTORE_PATH = REMOTE_AUTH + "truststore.path";
77+
static final String CONFIG_TRUSTSTORE_PASSWORD = REMOTE_AUTH + "truststore.password";
78+
static final String CONFIG_TRUSTSTORE_TYPE = REMOTE_AUTH + "truststore.type";
79+
static final String DEFAULT_TRUSTSTORE_TYPE = "JKS";
80+
static final String WILDCARD = "*";
7681
static final String TRACE_ID = "trace_id";
7782
static final String REQUEST_ID_HEADER_NAME = "X-Request-Id";
7883

79-
8084
private String remoteAuthUrl;
8185
private List<String> includeHeaders;
8286
private String cacheKeyHeader;
@@ -94,6 +98,10 @@ public class RemoteAuthFilter implements Filter {
9498
AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME, AuditConstants.KNOX_COMPONENT_NAME );
9599
private final RemoteAuthMessages LOGGER = MessagesFactory.get( RemoteAuthMessages.class );
96100

101+
private String truststorePath;
102+
private String truststorePassword;
103+
private String truststoreType;
104+
97105
@Override
98106
public void init(FilterConfig filterConfig) throws ServletException {
99107
remoteAuthUrl = filterConfig.getInitParameter(CONFIG_REMOTE_AUTH_URL);
@@ -123,6 +131,13 @@ public void init(FilterConfig filterConfig) throws ServletException {
123131
} else {
124132
groupHeaders = Arrays.asList(groupHeaderParam.split("\\s*,\\s*"));
125133
}
134+
135+
truststorePath = filterConfig.getInitParameter(CONFIG_TRUSTSTORE_PATH);
136+
truststorePassword = filterConfig.getInitParameter(CONFIG_TRUSTSTORE_PASSWORD);
137+
truststoreType = filterConfig.getInitParameter(CONFIG_TRUSTSTORE_TYPE);
138+
if (truststoreType == null || truststoreType.isEmpty()) {
139+
truststoreType = DEFAULT_TRUSTSTORE_TYPE;
140+
}
126141
}
127142

128143
public SSLSocketFactory createSSLSocketFactory(KeyStore trustStore) throws Exception {
@@ -202,33 +217,46 @@ private HttpURLConnection getHttpURLConnection(ServletContext servletContext) th
202217
if (services != null) {
203218
KeystoreService keystoreService = services.getService(ServiceType.KEYSTORE_SERVICE);
204219
if (keystoreService != null) {
205-
try {
206-
truststore = keystoreService.getTruststoreForHttpClient();
207-
if (truststore == null) {
208-
truststore = keystoreService.getKeystoreForGateway();
209-
}
210-
} catch (KeystoreServiceException e) {
211-
LOGGER.failedToLoadTruststore(e.getMessage(), e);
212-
}
220+
truststore = getTrustStore(truststore, keystoreService);
213221
}
214222
}
215223
HttpURLConnection connection;
216224
if (httpURLConnection == null) {
217225
URL url = new URL(remoteAuthUrl);
218226
connection = (HttpURLConnection) url.openConnection();
219227
if (truststore != null) {
220-
try {
221-
((HttpsURLConnection) connection).setSSLSocketFactory(createSSLSocketFactory(truststore));
222-
} catch (Exception e) {
223-
throw new RuntimeException(e);
224-
}
228+
try {
229+
((HttpsURLConnection) connection).setSSLSocketFactory(createSSLSocketFactory(truststore));
230+
} catch (Exception e) {
231+
throw new RuntimeException(e);
232+
}
225233
}
226234
} else {
227235
connection = httpURLConnection;
228236
}
229237
return connection;
230238
}
231239

240+
private KeyStore getTrustStore(KeyStore truststore, KeystoreService keystoreService) throws IOException {
241+
try {
242+
// Try topology-specific truststore first if configured
243+
if (truststorePath != null && !truststorePath.isEmpty()) {
244+
truststore = keystoreService.loadTruststore(truststorePath, truststoreType, truststorePassword);
245+
}
246+
// Fall back to gateway-level truststore
247+
if (truststore == null) {
248+
truststore = keystoreService.getTruststoreForHttpClient();
249+
if (truststore == null) {
250+
truststore = keystoreService.getKeystoreForGateway();
251+
}
252+
}
253+
} catch (KeystoreServiceException e) {
254+
LOGGER.failedToLoadTruststore(e.getMessage(), e);
255+
throw new IOException("Failed to load truststore: ", e);
256+
}
257+
return truststore;
258+
}
259+
232260
private void continueWithEstablishedSecurityContext(Subject subject, final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain) throws IOException, ServletException {
233261
try {
234262
Subject.doAs(

gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java

+152-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
import org.apache.knox.gateway.security.GroupPrincipal;
2121
import org.apache.knox.gateway.security.PrimaryPrincipal;
22+
import org.apache.knox.gateway.services.GatewayServices;
23+
import org.apache.knox.gateway.services.ServiceType;
24+
import org.apache.knox.gateway.services.security.KeystoreService;
25+
import org.apache.knox.gateway.services.security.KeystoreServiceException;
2226
import org.apache.knox.test.mock.MockServletContext;
2327
import org.easymock.EasyMock;
2428
import org.junit.Before;
@@ -28,6 +32,7 @@
2832
import javax.security.auth.Subject;
2933
import javax.servlet.FilterChain;
3034
import javax.servlet.FilterConfig;
35+
import javax.servlet.ServletContext;
3136
import javax.servlet.ServletException;
3237
import javax.servlet.ServletRequest;
3338
import javax.servlet.ServletResponse;
@@ -45,17 +50,19 @@
4550
import java.util.List;
4651
import java.util.Map;
4752
import java.util.Set;
53+
import java.security.KeyStore;
4854

4955
import static org.junit.Assert.assertEquals;
5056
import static org.junit.Assert.assertFalse;
5157
import static org.junit.Assert.assertTrue;
5258

59+
@SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
5360
public class RemoteAuthFilterTest {
5461

5562
public static final String BEARER_INVALID_TOKEN = "Bearer invalid-token";
5663
public static final String BEARER_VALID_TOKEN = "Bearer valid-token";
57-
public static final String URL_SUCCESS = "http://example.com/auth";
58-
private static final String URL_FAIL = "http://example.com/authfail";
64+
public static final String URL_SUCCESS = "https://example.com/auth";
65+
private static final String URL_FAIL = "https://example.com/authfail";
5966
public static final String X_AUTHENTICATED_USER = "X-Authenticated-User";
6067
public static final String X_AUTHENTICATED_GROUP = "X-Authenticated-Group";
6168
public static final String X_AUTHENTICATED_GROUP_2 = "X-Authenticated-Group-2";
@@ -65,22 +72,52 @@ public class RemoteAuthFilterTest {
6572
private HttpServletRequest requestMock;
6673
private HttpServletResponse responseMock;
6774
private TestFilterChain chainMock;
75+
private GatewayServices gatewayServicesMock;
76+
private KeystoreService keystoreServiceMock;
77+
private ServletContext servletContextMock;
78+
6879
@Before
69-
public void setUp() {
70-
FilterConfig filterConfigMock = EasyMock.createNiceMock(FilterConfig.class);
80+
public void createMocks() {
7181
requestMock = EasyMock.createMock(HttpServletRequest.class);
7282
responseMock = EasyMock.createMock(HttpServletResponse.class);
83+
}
84+
85+
private void setUp(String trustStorePath, String trustStorePass, String trustStoreType) {
86+
// Reset existing mocks
87+
EasyMock.reset(requestMock, responseMock);
88+
89+
FilterConfig filterConfigMock = EasyMock.createNiceMock(FilterConfig.class);
7390
chainMock = new TestFilterChain();
7491

75-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.url")).andReturn("http://example.com/auth").anyTimes();
76-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.include.headers")).andReturn("Authorization").anyTimes();
77-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.cache.key")).andReturn("Authorization").anyTimes();
78-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.expire.after")).andReturn("5").anyTimes();
79-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.user.header")).andReturn(X_AUTHENTICATED_USER).anyTimes();
80-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.group.header"))
92+
// Create and configure Gateway Services mocks
93+
gatewayServicesMock = EasyMock.createNiceMock(GatewayServices.class);
94+
keystoreServiceMock = EasyMock.createNiceMock(KeystoreService.class);
95+
servletContextMock = EasyMock.createNiceMock(ServletContext.class);
96+
97+
// Set up Gateway Services expectations
98+
EasyMock.expect(gatewayServicesMock.getService(ServiceType.KEYSTORE_SERVICE))
99+
.andReturn(keystoreServiceMock)
100+
.anyTimes();
101+
EasyMock.expect(servletContextMock.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE))
102+
.andReturn(gatewayServicesMock)
103+
.anyTimes();
104+
105+
// Basic config
106+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_REMOTE_AUTH_URL)).andReturn("https://example.com/auth").anyTimes();
107+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_INCLUDE_HEADERS)).andReturn("Authorization").anyTimes();
108+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.DEFAULT_CACHE_KEY_HEADER)).andReturn("Authorization").anyTimes();
109+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_EXPIRE_AFTER)).andReturn("5").anyTimes();
110+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_USER_HEADER)).andReturn(X_AUTHENTICATED_USER).anyTimes();
111+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_GROUP_HEADER))
81112
.andReturn(X_AUTHENTICATED_GROUP + "," + X_AUTHENTICATED_GROUP_2 + ",X-Custom-Group-*").anyTimes();
82113

83-
EasyMock.replay(filterConfigMock);
114+
// Trust store config
115+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_PATH)).andReturn(trustStorePath).anyTimes();
116+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_PASSWORD)).andReturn(trustStorePass).anyTimes();
117+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_TYPE)).andReturn(trustStoreType).anyTimes();
118+
119+
// Only replay the mocks that won't need additional expectations
120+
EasyMock.replay(filterConfigMock, gatewayServicesMock, servletContextMock);
84121

85122
filter = new RemoteAuthFilter();
86123
try {
@@ -90,6 +127,11 @@ public void setUp() {
90127
}
91128
}
92129

130+
// Default setup method for backward compatibility
131+
private void setUp() {
132+
setUp(null, null, null);
133+
}
134+
93135
private void setupURLConnection(String url) {
94136
try {
95137
filter.httpURLConnection = new MockHttpURLConnection(new URL(url));
@@ -100,6 +142,8 @@ private void setupURLConnection(String url) {
100142

101143
@Test
102144
public void successfulAuthentication() throws Exception {
145+
setUp();
146+
103147
EasyMock.expect(requestMock.getServletContext()).andReturn(new MockServletContext()).anyTimes();
104148
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_VALID_TOKEN).anyTimes();
105149
EasyMock.expect(responseMock.getStatus()).andReturn(200).anyTimes();
@@ -134,6 +178,8 @@ public void successfulAuthentication() throws Exception {
134178

135179
@Test
136180
public void authenticationFailsWithInvalidToken() throws Exception {
181+
setUp();
182+
137183
EasyMock.expect(requestMock.getServletContext()).andReturn(new MockServletContext()).anyTimes();
138184
EasyMock.expect(responseMock.getStatus()).andReturn(401).anyTimes();
139185
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_INVALID_TOKEN).anyTimes();
@@ -160,11 +206,12 @@ public void authenticationFailsWithInvalidToken() throws Exception {
160206

161207
@Test
162208
public void testCacheBehavior() throws Exception {
209+
setUp();
210+
163211
String principalName = "lmccayiv";
164212
String groupNames = "admin2,scientists";
165213
Subject subject = new Subject();
166214
subject.getPrincipals().add(new PrimaryPrincipal(principalName));
167-
// Add groups to the principal if available
168215
Arrays.stream(groupNames.split(",")).forEach(groupName -> subject.getPrincipals()
169216
.add(new GroupPrincipal(groupName)));
170217
filter.setCachedSubject(BEARER_VALID_TOKEN, subject);
@@ -202,9 +249,10 @@ public void testCacheBehavior() throws Exception {
202249

203250
@Test
204251
public void testTraceIdPropagation() throws Exception {
252+
setUp();
253+
205254
String expectedTraceId = "test-trace-123";
206255

207-
// Set up mocks
208256
EasyMock.expect(requestMock.getServletContext())
209257
.andReturn(new MockServletContext())
210258
.anyTimes();
@@ -250,6 +298,8 @@ public String getRequestProperty(String key) {
250298

251299
@Test
252300
public void successfulAuthenticationWithMultipleGroups() throws Exception {
301+
setUp();
302+
253303
EasyMock.expect(requestMock.getServletContext()).andReturn(new MockServletContext()).anyTimes();
254304
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_VALID_TOKEN).anyTimes();
255305
EasyMock.expect(responseMock.getStatus()).andReturn(200).anyTimes();
@@ -287,6 +337,95 @@ public void successfulAuthenticationWithMultipleGroups() throws Exception {
287337
}
288338
}
289339

340+
@Test
341+
public void testSuccessfulHttpsRequestWithTrustStore() throws Exception {
342+
// Setup with valid trust store configuration
343+
setUp("/path/to/truststore.jks", "trustpass", "JKS");
344+
345+
KeyStore testTruststore = KeyStore.getInstance("JKS");
346+
EasyMock.expect(keystoreServiceMock.loadTruststore("/path/to/truststore.jks", "JKS", "trustpass"))
347+
.andReturn(testTruststore)
348+
.anyTimes();
349+
350+
EasyMock.expect(requestMock.getServletContext())
351+
.andReturn(servletContextMock)
352+
.anyTimes();
353+
EasyMock.expect(requestMock.getHeader("Authorization"))
354+
.andReturn(BEARER_VALID_TOKEN)
355+
.anyTimes();
356+
EasyMock.expect(responseMock.getStatus())
357+
.andReturn(200)
358+
.anyTimes();
359+
responseMock.sendError(EasyMock.eq(HttpServletResponse.SC_UNAUTHORIZED), EasyMock.anyString());
360+
EasyMock.expectLastCall().andThrow(new AssertionError("Authentication should be successful, but was not.")).anyTimes();
361+
362+
EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
363+
364+
setupURLConnection("https://example.com/auth");
365+
filter.doFilter(requestMock, responseMock, chainMock);
366+
367+
assertTrue("Filter chain should have been called", chainMock.doFilterCalled);
368+
}
369+
370+
@Test
371+
public void testHttpsRequestWithoutTrustStore() throws Exception {
372+
// Setup without trust store configuration
373+
setUp(null, null, null);
374+
375+
KeyStore defaultTruststore = KeyStore.getInstance("JKS");
376+
EasyMock.expect(keystoreServiceMock.getTruststoreForHttpClient())
377+
.andReturn(defaultTruststore)
378+
.anyTimes();
379+
380+
EasyMock.expect(requestMock.getServletContext())
381+
.andReturn(servletContextMock)
382+
.anyTimes();
383+
EasyMock.expect(requestMock.getHeader("Authorization"))
384+
.andReturn(BEARER_VALID_TOKEN)
385+
.anyTimes();
386+
EasyMock.expect(responseMock.getStatus())
387+
.andReturn(200)
388+
.anyTimes();
389+
responseMock.sendError(EasyMock.eq(HttpServletResponse.SC_UNAUTHORIZED), EasyMock.anyString());
390+
EasyMock.expectLastCall().andThrow(new AssertionError("Authentication should be successful, but was not.")).anyTimes();
391+
392+
EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
393+
394+
setupURLConnection("https://example.com/auth");
395+
filter.doFilter(requestMock, responseMock, chainMock);
396+
397+
assertTrue("Filter chain should have been called with default trust store", chainMock.doFilterCalled);
398+
}
399+
400+
@Test
401+
public void testHttpsRequestWithInvalidTrustStoreConfig() throws Exception {
402+
// Setup with invalid trust store configuration
403+
setUp("/nonexistent/path/truststore.jks", "password", "JKS");
404+
405+
EasyMock.expect(keystoreServiceMock.loadTruststore("/nonexistent/path/truststore.jks", "JKS", "password"))
406+
.andThrow(new KeystoreServiceException("Failed to load truststore"))
407+
.anyTimes();
408+
409+
EasyMock.expect(requestMock.getServletContext())
410+
.andReturn(servletContextMock)
411+
.anyTimes();
412+
EasyMock.expect(requestMock.getHeader("Authorization"))
413+
.andReturn(BEARER_VALID_TOKEN)
414+
.anyTimes();
415+
EasyMock.expect(responseMock.getStatus())
416+
.andReturn(500)
417+
.anyTimes();
418+
responseMock.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error processing authentication request");
419+
EasyMock.expectLastCall().once();
420+
421+
EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
422+
423+
filter.doFilter(requestMock, responseMock, chainMock);
424+
425+
assertFalse("Filter chain should not have been called", chainMock.doFilterCalled);
426+
EasyMock.verify(responseMock);
427+
}
428+
290429
public static class MockHttpURLConnection extends HttpURLConnection {
291430
private final URL url;
292431
private int responseCode;

0 commit comments

Comments
 (0)