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