diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index 88c5ad7157..92dc225815 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -22,6 +22,7 @@
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.provider.federation.jwt.JWTMessages;
import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.security.CommonTokenConstants;
import org.apache.knox.gateway.services.security.token.UnknownTokenException;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
@@ -54,10 +55,10 @@ public class JWTFederationFilter extends AbstractJWTFilter {
private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class );
/* A semicolon separated list of paths that need to bypass authentication */
public static final String JWT_UNAUTHENTICATED_PATHS_PARAM = "jwt.unauthenticated.path.list";
- public static final String GRANT_TYPE = "grant_type";
- public static final String CLIENT_CREDENTIALS = "client_credentials";
- public static final String CLIENT_SECRET = "client_secret";
- public static final String CLIENT_ID = "client_id";
+ public static final String GRANT_TYPE = CommonTokenConstants.GRANT_TYPE;
+ public static final String CLIENT_CREDENTIALS = CommonTokenConstants.CLIENT_CREDENTIALS;
+ public static final String CLIENT_SECRET = CommonTokenConstants.CLIENT_CREDENTIALS;
+ public static final String CLIENT_ID = CommonTokenConstants.CLIENT_ID;
public static final String INVALID_CLIENT_SECRET = "Error while parsing the received client secret";
public static final String MISMATCHING_CLIENT_ID_AND_CLIENT_SECRET = "Client credentials flow with mismatching client_id and client_secret";
public static final String REFRESH_TOKEN = "refresh_token";
diff --git a/gateway-service-definitions/src/main/resources/services/iceberg-rest/0.0.1/service.xml b/gateway-service-definitions/src/main/resources/services/iceberg-rest/0.0.1/service.xml
index f699b33d80..99dbeaee05 100644
--- a/gateway-service-definitions/src/main/resources/services/iceberg-rest/0.0.1/service.xml
+++ b/gateway-service-definitions/src/main/resources/services/iceberg-rest/0.0.1/service.xml
@@ -22,12 +22,11 @@
ICEBERG-REST
Apache Iceberg REST Catalog API
-
-
+
shouldIncludePrincipalAndGroups
true
diff --git a/gateway-service-restcatalog/pom.xml b/gateway-service-restcatalog/pom.xml
new file mode 100644
index 0000000000..4f67ee25be
--- /dev/null
+++ b/gateway-service-restcatalog/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+ 4.0.0
+
+ org.apache.knox
+ gateway
+ 3.0.0-SNAPSHOT
+
+
+ gateway-service-restcatalog
+
+
+
+ org.apache.knox
+ gateway-spi
+ compile
+
+
+ javax.servlet
+ javax.servlet-api
+ compile
+
+
+ org.apache.httpcomponents
+ httpclient
+ compile
+
+
+ org.apache.httpcomponents
+ httpcore
+ compile
+
+
+ org.apache.knox
+ gateway-provider-ha
+
+
+ org.apache.knox
+ gateway-i18n
+
+
+ com.github.ben-manes.caffeine
+ caffeine
+
+
+ org.eclipse.jetty
+ jetty-http
+ test
+
+
+ org.apache.knox
+ gateway-test-utils
+ test
+
+
+
+
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java
new file mode 100644
index 0000000000..ca9951d8ef
--- /dev/null
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatch.java
@@ -0,0 +1,52 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.knox.gateway.dispatch.ConfigurableDispatch;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A Dispatch implementation that supports adding token metadata to the outbound request headers.
+ */
+public class RestCatalogDispatch extends ConfigurableDispatch {
+
+ private final TokenMetadataHeaderHandler headerHandler;
+
+ public RestCatalogDispatch(FilterConfig filterConfig) {
+ headerHandler = new TokenMetadataHeaderHandler(filterConfig);
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ }
+
+ @Override
+ protected void executeRequest(HttpUriRequest outboundRequest,
+ HttpServletRequest inboundRequest,
+ HttpServletResponse outboundResponse) throws IOException {
+ headerHandler.applyHeadersToRequest(inboundRequest, outboundRequest);
+ super.executeRequest(outboundRequest, inboundRequest, outboundResponse);
+ }
+
+}
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java
new file mode 100644
index 0000000000..eb0290a292
--- /dev/null
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatch.java
@@ -0,0 +1,49 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.knox.gateway.ha.dispatch.ConfigurableHADispatch;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class RestCatalogHaDispatch extends ConfigurableHADispatch {
+
+ static final String SERVICE_ROLE = "ICEBERG-REST";
+
+ private final TokenMetadataHeaderHandler headerHandler;
+
+ public RestCatalogHaDispatch(final FilterConfig filterConfig) {
+ setServiceRole(SERVICE_ROLE);
+ headerHandler = new TokenMetadataHeaderHandler(filterConfig);
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ }
+
+ @Override
+ protected void executeRequest(HttpUriRequest outboundRequest, HttpServletRequest inboundRequest, HttpServletResponse outboundResponse) throws IOException {
+ headerHandler.applyHeadersToRequest(inboundRequest, outboundRequest);
+ super.executeRequest(outboundRequest, inboundRequest, outboundResponse);
+ }
+}
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java
new file mode 100644
index 0000000000..3511efe9c9
--- /dev/null
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/TokenMetadataHeaderHandler.java
@@ -0,0 +1,184 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.security.CommonTokenConstants;
+import org.apache.knox.gateway.service.restcatalog.i18n.TokenMetadataHandlerMessages;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class TokenMetadataHeaderHandler {
+ static final String TOKEN_METADATA_PARAM = "token-metadata-headers";
+ static final String METADATA_HEADER_PREFIX_PARAM = "metadata-header-prefix";
+
+ static final String DEFAULT_HEADER_PREFIX = "X-Knox-Meta-";
+
+ private static final TokenMetadataHandlerMessages LOG = MessagesFactory.get(TokenMetadataHandlerMessages.class);
+
+ private static final String INVALID_CLIENT_SECRET = "Error while parsing the received client secret";
+
+ private final TokenStateService tss;
+
+ private final String headerPrefix;
+
+ private final Set metadataHeaderConfig = new HashSet<>();
+
+ private final Cache metadataCache = Caffeine.newBuilder().maximumSize(100).build();
+
+ TokenMetadataHeaderHandler(final FilterConfig filterConfig) {
+ ServletContext sc = filterConfig.getServletContext();
+ GatewayServices gatewayServices = (GatewayServices) sc.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ tss = gatewayServices.getService(ServiceType.TOKEN_STATE_SERVICE);
+
+ metadataHeaderConfig.addAll(getMetadataHeaderConfig(filterConfig));
+
+ headerPrefix = getMetadataHeaderPrefixConfig(filterConfig);
+ }
+
+ void applyHeadersToRequest(final HttpServletRequest inboundRequest,
+ final HttpUriRequest outboundRequest) {
+ final String clientId = getClientID(inboundRequest);
+ if (clientId != null) {
+ Map metadata = getMetadata(tss, clientId);
+ for (String key : metadataHeaderConfig) {
+ if (metadata.containsKey(key)) {
+ outboundRequest.setHeader(headerPrefix + key, metadata.get(key));
+ }
+ }
+ }
+ }
+
+ private Set getMetadataHeaderConfig(final FilterConfig filterConfig) {
+ Set metadataForHeaders = new HashSet<>();
+
+ // Add the default metadata element to be included in the outbound request headers
+ metadataForHeaders.add("userName");
+
+ // Parse the configured token metadata elements which should be included as outbound request headers
+ String tokenMetadataHeadersConfig = filterConfig.getInitParameter(TOKEN_METADATA_PARAM);
+ String[] tokenMetadataHeaderNames = tokenMetadataHeadersConfig.split(",");
+ for (String metadataName : tokenMetadataHeaderNames) {
+ metadataForHeaders.add(metadataName.trim());
+ }
+ return metadataForHeaders;
+ }
+
+ private String getMetadataHeaderPrefixConfig(final FilterConfig filterConfig) {
+ String value = DEFAULT_HEADER_PREFIX;
+ String configured = filterConfig.getInitParameter(METADATA_HEADER_PREFIX_PARAM);
+ if (configured != null) {
+ configured = configured.trim();
+ if (!configured.isEmpty()) {
+ value = configured;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Get the clientId from the client credentials in the request body content.
+ *
+ * @param request The ServletRequest.
+ *
+ * @return The clientId from the request.
+ */
+ String getClientID(ServletRequest request) {
+ String clientId = null;
+
+ String clientSecret = getClientSecretFromRequestBody(request);
+ if (clientSecret != null) {
+ try {
+ final String[] decodedSecret = decodeBase64(clientSecret).split("::");
+ clientId = decodeBase64(decodedSecret[0]);
+ } catch (Exception e) {
+ LOG.invalidClientSecret(e);
+ throw new SecurityException(INVALID_CLIENT_SECRET, e);
+ }
+ }
+
+ return clientId;
+ }
+
+ /**
+ * Get the client secret from the request body.
+ *
+ * @param request The ServletRequest with the body content.
+ *
+ * @return The client secret.
+ */
+ private String getClientSecretFromRequestBody(ServletRequest request) {
+ final String grantType = request.getParameter(CommonTokenConstants.GRANT_TYPE);
+ String clientSecret = null;
+ if (CommonTokenConstants.CLIENT_CREDENTIALS.equals(grantType)) {
+ clientSecret = request.getParameter(CommonTokenConstants.CLIENT_SECRET);
+ }
+ return clientSecret;
+ }
+
+ private String decodeBase64(final String encoded) {
+ return new String(Base64.getDecoder().decode(encoded.getBytes(UTF_8)), UTF_8);
+ }
+
+ /**
+ * Get the token metadata associated with the specified clientId.
+ *
+ * @param clientId The clientId for which the token metadata is requested.
+ *
+ * @return A map of the associated token metadata.
+ */
+ private Map getMetadata(final TokenStateService tss, final String clientId) {
+ Map metadata = Collections.emptyMap();
+
+ // Get the associated token metadata
+ TokenMetadata tm = metadataCache.get(clientId, k -> {
+ try {
+ return tss.getTokenMetadata(clientId);
+ } catch (UnknownTokenException e) {
+ LOG.invalidClientId(clientId, e);
+ return null;
+ }
+ });
+ if (tm != null) {
+ metadata = tm.getMetadataMap();
+ } else {
+ LOG.noMetadataForClientId(clientId);
+ }
+
+ return metadata;
+ }
+
+}
diff --git a/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/i18n/TokenMetadataHandlerMessages.java b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/i18n/TokenMetadataHandlerMessages.java
new file mode 100644
index 0000000000..196d1a7879
--- /dev/null
+++ b/gateway-service-restcatalog/src/main/java/org/apache/knox/gateway/service/restcatalog/i18n/TokenMetadataHandlerMessages.java
@@ -0,0 +1,40 @@
+/*
+ * 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.knox.gateway.service.restcatalog.i18n;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.i18n.messages.StackTrace;
+
+@Messages(logger = "org.apache.knox.gateway")
+public interface TokenMetadataHandlerMessages {
+
+ @Message(level = MessageLevel.INFO, text = "Configured metadata header prefix: {0}")
+ void configuredMetatadataHeaderPrefix(String prefix);
+
+ @Message(level = MessageLevel.WARN, text = "There is no metadata associated with client ID: {0}")
+ void noMetadataForClientId(String clientId);
+
+ @Message(level = MessageLevel.ERROR, text = "Invalid client secret: {0}")
+ void invalidClientSecret(@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "Invalid client ID: {0} ; {1}")
+ void invalidClientId(String clientId, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+}
diff --git a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTest.java b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTest.java
new file mode 100644
index 0000000000..508d9fde1f
--- /dev/null
+++ b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.knox.gateway.security.CommonTokenConstants;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+
+import static org.junit.Assert.assertEquals;
+
+public class RestCatalogDispatchTest {
+
+ private final RestCatalogDispatchTestUtils testUtils = new RestCatalogDispatchTestUtils();
+
+ /**
+ * If there are no client credentials in the request, then the metadata access and header additions should be
+ * skipped altogether.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testNoClientCredentialsInRequest() throws Exception {
+ HttpUriRequest outboundRequest =
+ testUtils.doTest(Collections.emptyMap(), "email,test", null, null, null);
+
+ assertEquals("There should be no headers in the outbound request because the inbound request does not include client credentials.",
+ 0, outboundRequest.getAllHeaders().length);
+ }
+
+ /**
+ * Verify that metadata items configured for inclusion in outbound requests as headers are indeed included iff the
+ * metadata item actually exists for the client_id, and that no other metadata items manifest as headers except the
+ * default userName metadata.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testIncludeConfiguredMetatadataAsHeaders() throws Exception {
+ final String client_id = "4606f5e1-9d02-41a1-9071-26a9e74e4ab9";
+ final String clientSecret =
+ "TkRZd05tWTFaVEV0T1dRd01pMDBNV0V4TFRrd056RXRNalpoT1dVM05HVTBZV0k1OjpNV1JrWkRBMk1tVXRPV0ppTnkwMFkyWTVMVGxqTUdJdFpqWXhZalV5WTJGa1lURmw";
+
+ final String metadata_email = "user@host.com";
+ final String metadata_userName = "someuser";
+ final String metadata_category = "test category";
+ final String metadata_arbitrary = "somevalue";
+
+ Map metaMap = new HashMap<>();
+ metaMap.put("email", metadata_email);
+ metaMap.put("userName", metadata_userName);
+ metaMap.put("category", metadata_category);
+ metaMap.put("arbitrary", metadata_arbitrary);
+ metaMap.put("enabled", "true");
+
+ HttpUriRequest outboundRequest =
+ testUtils.doTest(metaMap, "email, test", CommonTokenConstants.CLIENT_CREDENTIALS, client_id, clientSecret);
+
+ assertEquals("Unexpected number of headers in the outbound request",2, outboundRequest.getAllHeaders().length);
+ assertEquals("userName is not configured as metadata which should result in an outbound header, but it is included by default.",
+ 1, outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "userName").length);
+ assertEquals("email is configured as metadata which should result in an outbound header.",
+ 1, outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "email").length);
+ assertEquals("Even though the test metadata is configured, there should be no header since there is no actual such metadata.",
+ 0,outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "arbitrary").length);
+ assertEquals("Even though there is metadata by this name, it is not configured to be conveyed as a header, so it should be ignored.",
+ 0, outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "test").length);
+ }
+
+ /**
+ * Verify that metadata items configured for inclusion in outbound requests as headers are indeed included iff the
+ * metadata item actually exists for the client_id, and that no other metadata items manifest as headers except the
+ * default userName metadata.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testIncludeConfiguredMetatadataAsHeadersWithConfigurablePrefix() throws Exception {
+ final String customHeaderPrefix = "Test-Header-Prefix-";
+ final String client_id = "4606f5e1-9d02-41a1-9071-26a9e74e4ab9";
+ final String clientSecret =
+ "TkRZd05tWTFaVEV0T1dRd01pMDBNV0V4TFRrd056RXRNalpoT1dVM05HVTBZV0k1OjpNV1JrWkRBMk1tVXRPV0ppTnkwMFkyWTVMVGxqTUdJdFpqWXhZalV5WTJGa1lURmw";
+
+ final String metadata_email = "user@host.com";
+ final String metadata_userName = "someuser";
+ final String metadata_category = "test category";
+ final String metadata_arbitrary = "somevalue";
+
+ Map metaMap = new HashMap<>();
+ metaMap.put("email", metadata_email);
+ metaMap.put("userName", metadata_userName);
+ metaMap.put("category", metadata_category);
+ metaMap.put("arbitrary", metadata_arbitrary);
+ metaMap.put("enabled", "true");
+
+ HttpUriRequest outboundRequest =
+ testUtils.doTest(metaMap, "email,arbitrary", customHeaderPrefix, CommonTokenConstants.CLIENT_CREDENTIALS, client_id, clientSecret);
+
+ assertEquals("Unexpected number of headers in the outbound request",3, outboundRequest.getAllHeaders().length);
+ assertEquals("userName is not configured as metadata which should result in an outbound header, but it is included by default.",
+ 1, outboundRequest.getHeaders(customHeaderPrefix + "userName").length);
+ assertEquals("email is configured as metadata which should result in an outbound header.",
+ 1, outboundRequest.getHeaders(customHeaderPrefix + "email").length);
+ assertEquals("Even though the test metadata is configured, there should be no header since there is no actual such metadata.",
+ 0,outboundRequest.getHeaders(customHeaderPrefix + "test").length);
+ assertEquals("Since there is metadata by this name, and it is configured to be conveyed as a header, it should be present.",
+ 1, outboundRequest.getHeaders(customHeaderPrefix + "arbitrary").length);
+ }
+
+}
diff --git a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java
new file mode 100644
index 0000000000..80180ee96e
--- /dev/null
+++ b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogDispatchTestUtils.java
@@ -0,0 +1,152 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.knox.gateway.security.CommonTokenConstants;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.test.mock.MockHttpServletResponse;
+import org.easymock.EasyMock;
+import org.eclipse.jetty.http.HttpMethod;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+import static org.easymock.EasyMock.anyString;
+
+public class RestCatalogDispatchTestUtils {
+
+ static FilterConfig createBaseMocks(final Map metaMap,
+ final String metaConfig,
+ final String client_id) throws Exception {
+ return createBaseMocks(metaMap, metaConfig, null, client_id);
+ }
+
+ static FilterConfig createBaseMocks(final Map metaMap,
+ final String metaConfig,
+ final String headerPrefix,
+ final String client_id) throws Exception {
+ TokenMetadata mockMetadata = EasyMock.createNiceMock(TokenMetadata.class);
+ EasyMock.expect(mockMetadata.getMetadataMap()).andReturn(metaMap).anyTimes();
+ EasyMock.replay(mockMetadata);
+
+ TokenStateService mockTss = EasyMock.createNiceMock(TokenStateService.class);
+ if (client_id != null) {
+ EasyMock.expect(mockTss.getTokenMetadata(client_id)).andReturn(mockMetadata).anyTimes();
+ } else {
+ EasyMock.expect(mockTss.getTokenMetadata(anyString())).andReturn(mockMetadata).anyTimes();
+ }
+ EasyMock.replay(mockTss);
+
+ GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class);
+ EasyMock.expect(gws.getService(ServiceType.TOKEN_STATE_SERVICE)).andReturn(mockTss).anyTimes();
+ EasyMock.replay(gws);
+
+ ServletContext sc = EasyMock.createNiceMock(ServletContext.class);
+ EasyMock.expect(sc.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gws).anyTimes();
+ EasyMock.replay(sc);
+
+ FilterConfig filterConfig = EasyMock.createNiceMock(FilterConfig.class);
+ EasyMock.expect(filterConfig.getServletContext()).andReturn(sc).anyTimes();
+ EasyMock.expect(filterConfig.getInitParameter(TokenMetadataHeaderHandler.TOKEN_METADATA_PARAM)).andReturn(metaConfig).anyTimes();
+ EasyMock.expect(filterConfig.getInitParameter(TokenMetadataHeaderHandler.METADATA_HEADER_PREFIX_PARAM)).andReturn(headerPrefix).anyTimes();
+ EasyMock.replay(filterConfig);
+
+ return filterConfig;
+ }
+
+ /**
+ *
+ * @param metaMap A Map of test token metadata
+ * @param metaConfig The value of the token-metadata-headers service configuration param
+ * @param grantType The grant type for the incoming request body
+ * @param client_id A client_id for the incoming request body
+ * @param clientSecret A client_secret for the incoming request body
+ *
+ * @return The resulting outbound request object.
+ *
+ * @throws Exception
+ */
+ HttpUriRequest doTest(final Map metaMap,
+ final String metaConfig,
+ final String grantType,
+ final String client_id,
+ final String clientSecret) throws Exception {
+ return doTest(metaMap, metaConfig, null, grantType, client_id, clientSecret);
+ }
+
+ /**
+ *
+ * @param metaMap A Map of test token metadata
+ * @param metaConfig The value of the token-metadata-headers service configuration param
+ * @param grantType The grant type for the incoming request body
+ * @param client_id A client_id for the incoming request body
+ * @param clientSecret A client_secret for the incoming request body
+ *
+ * @return The resulting outbound request object.
+ *
+ * @throws Exception
+ */
+ HttpUriRequest doTest(final Map metaMap,
+ final String metaConfig,
+ final String headerPrefix,
+ final String grantType,
+ final String client_id,
+ final String clientSecret) throws Exception {
+ FilterConfig filterConfig = createBaseMocks(metaMap, metaConfig, headerPrefix, client_id);
+
+ HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+ EasyMock.expect(inboundRequest.getParameter(CommonTokenConstants.GRANT_TYPE)).andReturn(grantType).anyTimes();
+ EasyMock.expect(inboundRequest.getParameter(CommonTokenConstants.CLIENT_SECRET)).andReturn(clientSecret).anyTimes();
+ EasyMock.replay(inboundRequest);
+
+ RestCatalogDispatch dispatch = new RestCatalogDispatch(filterConfig);
+ dispatch.init();
+
+ HttpServletResponse outboundResponse = new MockHttpServletResponse();
+ HttpUriRequest outboundRequest = new TestHttpUriRequest();
+ try {
+ dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse);
+ } catch (Exception e) {
+ // Expected since the request is not complete and this test is only concerned with the outbound headers
+ }
+
+ return outboundRequest;
+ }
+
+
+ static class TestHttpUriRequest extends HttpRequestBase {
+ private HttpMethod method = HttpMethod.GET;
+
+ public void setMethod(HttpMethod method) {
+ this.method = method;
+ }
+
+ @Override
+ public String getMethod() {
+ return method.name();
+ }
+ }
+}
diff --git a/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java
new file mode 100644
index 0000000000..93f69f3906
--- /dev/null
+++ b/gateway-service-restcatalog/src/test/java/org/apache/knox/gateway/service/restcatalog/RestCatalogHaDispatchTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.knox.gateway.service.restcatalog;
+
+import org.apache.http.Header;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.knox.gateway.ha.provider.HaDescriptor;
+import org.apache.knox.gateway.ha.provider.HaProvider;
+import org.apache.knox.gateway.ha.provider.HaServiceConfig;
+import org.apache.knox.gateway.ha.provider.impl.DefaultHaProvider;
+import org.apache.knox.gateway.ha.provider.impl.HaDescriptorFactory;
+import org.apache.knox.gateway.security.CommonTokenConstants;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.WriteListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+
+public class RestCatalogHaDispatchTest {
+
+ private static final String LOCATION = "Location";
+
+ @Test
+ public void testConnectivityFailure() throws Exception {
+ final URI uri1 = new URI("https://host1.invalid");
+ final URI uri2 = new URI("https://host2.invalid");
+ final URI uri3 = new URI("https://host3.invalid");
+
+ final String client_id = "4606f5e1-9d02-41a1-9071-26a9e74e4ab9";
+ final String clientSecret =
+ "TkRZd05tWTFaVEV0T1dRd01pMDBNV0V4TFRrd056RXRNalpoT1dVM05HVTBZV0k1OjpNV1JrWkRBMk1tVXRPV0ppTnkwMFkyWTVMVGxqTUdJdFpqWXhZalV5WTJGa1lURmw";
+
+ final String metadata_email = "user@host.com";
+ final String metadata_userName = "someuser";
+ final String metadata_category = "test category";
+ final String metadata_arbitrary = "somevalue";
+
+ Map metaMap = new HashMap<>();
+ metaMap.put("email", metadata_email);
+ metaMap.put("userName", metadata_userName);
+ metaMap.put("category", metadata_category);
+ metaMap.put("arbitrary", metadata_arbitrary);
+ metaMap.put("enabled", "true");
+
+ List urls = new ArrayList<>();
+ urls.add(uri1);
+ urls.add(uri2);
+ urls.add(uri3);
+
+ HaServiceConfig serviceConfig =
+ HaDescriptorFactory.createServiceConfig(RestCatalogHaDispatch.SERVICE_ROLE,
+ "true",
+ "1",
+ "1000",
+ null,
+ null,
+ null,
+ null,
+ null,
+ null,
+ null);
+ HaProvider provider = createProvider(serviceConfig, urls);
+ FilterConfig filterConfig =
+ RestCatalogDispatchTestUtils.createBaseMocks(metaMap, "email, test", client_id);
+
+ RestCatalogDispatchTestUtils.TestHttpUriRequest outboundRequest =
+ new RestCatalogDispatchTestUtils.TestHttpUriRequest();
+ outboundRequest.setURI(uri1);
+ outboundRequest.setParams(new BasicHttpParams());
+
+ HttpServletRequest inboundRequest = EasyMock.createNiceMock(HttpServletRequest.class);
+ EasyMock.expect(inboundRequest.getParameter(CommonTokenConstants.GRANT_TYPE)).andReturn(CommonTokenConstants.CLIENT_CREDENTIALS).anyTimes();
+ EasyMock.expect(inboundRequest.getParameter(CommonTokenConstants.CLIENT_SECRET)).andReturn(clientSecret).anyTimes();
+ EasyMock.expect(inboundRequest.getRequestURL()).andReturn(new StringBuffer(uri2.toString())).once();
+ EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(0)).once();
+ EasyMock.expect(inboundRequest.getAttribute("dispatch.ha.failover.counter")).andReturn(new AtomicInteger(1)).once();
+
+ HttpServletResponse outboundResponse = EasyMock.createNiceMock(HttpServletResponse.class);
+ EasyMock.expect(outboundResponse.getOutputStream()).andAnswer(new IAnswer() {
+ @Override
+ public ServletOutputStream answer() throws Throwable {
+ return new ServletOutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ throw new IOException("unreachable-host.invalid");
+ }
+
+ @Override
+ public void setWriteListener(WriteListener arg0) {
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+ };
+ }
+ }).once();
+ EasyMock.replay(inboundRequest, outboundResponse);
+
+ assertEquals(uri1.toString(), provider.getActiveURL(RestCatalogHaDispatch.SERVICE_ROLE));
+ RestCatalogHaDispatch dispatch = new RestCatalogHaDispatch(filterConfig);
+ HttpClientBuilder builder = HttpClientBuilder.create();
+ CloseableHttpClient client = builder.build();
+ dispatch.setHttpClient(client);
+ dispatch.setHaProvider(provider);
+ dispatch.init();
+ long startTime = System.currentTimeMillis();
+ try {
+ dispatch.executeRequest(outboundRequest, inboundRequest, outboundResponse);
+ } catch (IOException e) {
+ //this is expected after the failover limit is reached
+ }
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ assertEquals(uri3.toString(), provider.getActiveURL(RestCatalogHaDispatch.SERVICE_ROLE));
+
+ // Validate the outbound request headers expected from the token metadata
+ assertEquals("Unexpected number of headers in the outbound request",2, outboundRequest.getAllHeaders().length);
+ Header[] usernameHeaders = outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "userName");
+ assertEquals("userName is not configured as metadata which should result in an outbound header, but it is included by default.",
+ 1, usernameHeaders.length);
+ assertEquals(metadata_userName, usernameHeaders[0].getValue());
+ Header[] emailHeaders = outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "email");
+ assertEquals("email is configured as metadata which should result in an outbound header.",
+ 1, emailHeaders.length);
+ assertEquals(metadata_email, emailHeaders[0].getValue());
+ assertEquals("Even though the test metadata is configured, there should be no header since there is no actual such metadata.",
+ 0,outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "test").length);
+ assertEquals("Even though there is metadata by this name, it is not configured to be conveyed as a header, so it should be ignored.",
+ 0, outboundRequest.getHeaders(TokenMetadataHeaderHandler.DEFAULT_HEADER_PREFIX + "arbitrary").length);
+
+ //test to make sure the sleep took place
+ Assert.assertTrue(elapsedTime > 1000);
+ }
+
+ private HaProvider createProvider(HaServiceConfig serviceConfig, List urls) throws Exception {
+ HaDescriptor descriptor = HaDescriptorFactory.createDescriptor();
+ descriptor.addServiceConfig(serviceConfig);
+
+ ArrayList urlList = new ArrayList<>();
+ for (URI uri : urls) {
+ urlList.add(uri.toString());
+ }
+
+ HaProvider provider = new DefaultHaProvider(descriptor);
+ provider.addHaService(RestCatalogHaDispatch.SERVICE_ROLE, urlList);
+
+ return provider;
+ }
+
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java
new file mode 100644
index 0000000000..1537f698b2
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java
@@ -0,0 +1,30 @@
+/*
+ * 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.knox.gateway.security;
+
+public interface CommonTokenConstants {
+
+ String GRANT_TYPE = "grant_type";
+
+ String CLIENT_CREDENTIALS = "client_credentials";
+
+ String CLIENT_ID = "client_id";
+
+ String CLIENT_SECRET = "client_secret";
+
+}
diff --git a/pom.xml b/pom.xml
index a9c4cc46eb..af8547cb8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -122,6 +122,7 @@
gateway-service-rm
gateway-service-storm
gateway-service-remoteconfig
+ gateway-service-restcatalog
gateway-service-definitions
gateway-shell
gateway-shell-launcher