Repository: knox Updated Branches: refs/heads/master 8bdd03cd6 -> c94d9b1e6 (forced update)
KNOX-1363 - Support service role-based whitelist for dispatches and redirects Project: http://git-wip-us.apache.org/repos/asf/knox/repo Commit: http://git-wip-us.apache.org/repos/asf/knox/commit/c94d9b1e Tree: http://git-wip-us.apache.org/repos/asf/knox/tree/c94d9b1e Diff: http://git-wip-us.apache.org/repos/asf/knox/diff/c94d9b1e Branch: refs/heads/master Commit: c94d9b1e68570760c7990f15e9d7e3670ed233c1 Parents: 89bf09f Author: Phil Zampino <[email protected]> Authored: Mon Jun 18 19:36:54 2018 -0400 Committer: Phil Zampino <[email protected]> Committed: Fri Jun 22 10:02:39 2018 -0700 ---------------------------------------------------------------------- gateway-release/home/conf/gateway-site.xml | 13 +- .../gateway/config/impl/GatewayConfigImpl.java | 21 ++ .../gateway/service/knoxsso/WebSSOResource.java | 9 +- .../service/knoxsso/WebSSOResourceTest.java | 156 ++++++++++- .../apache/knox/gateway/SpiGatewayMessages.java | 8 + .../knox/gateway/config/GatewayConfig.java | 16 ++ .../gateway/dispatch/GatewayDispatchFilter.java | 35 ++- .../knox/gateway/util/WhitelistUtils.java | 91 +++++++ .../dispatch/GatewayDispatchFilterTest.java | 263 +++++++++++++++++++ .../knox/gateway/util/WhitelistUtilsTest.java | 145 ++++++++++ .../apache/knox/gateway/GatewayTestConfig.java | 9 + 11 files changed, 746 insertions(+), 20 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-release/home/conf/gateway-site.xml ---------------------------------------------------------------------- diff --git a/gateway-release/home/conf/gateway-site.xml b/gateway-release/home/conf/gateway-site.xml index 64abf16..9894cf1 100644 --- a/gateway-release/home/conf/gateway-site.xml +++ b/gateway-release/home/conf/gateway-site.xml @@ -86,7 +86,7 @@ limitations under the License. </property> <!-- Knox Admin related config --> - <property> + <property> <name>gateway.knox.admin.groups</name> <value>admin</value> </property> @@ -128,5 +128,16 @@ limitations under the License. <name>gateway.group.config.hadoop.security.group.mapping.ldap.search.attr.group.name</name> <value>cn</value> </property> + <property> + <name>gateway.dispatch.whitelist.services</name> + <value>DATANODE,HBASEUI,HDFSUI,JOBHISTORYUI,NODEUI,RESOURCEMANAGER,WEBHBASE,WEBHDFS,YARNUI</value> + <description>The comma-delimited list of service roles for which the gateway.dispatch.whitelist should be applied.</description> + </property> + + <property> + <name>gateway.dispatch.whitelist</name> + <value></value> + <description>The whitelist to be applied for dispatches associated with the service roles specified by gateway.dispatch.whitelist.services.</description> + </property> </configuration> http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java ---------------------------------------------------------------------- diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java index 815bb95..0ff8b22 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java @@ -253,6 +253,9 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig { static final String AUTO_DEPLOY_TOPOLOGIES = GATEWAY_CONFIG_FILE_PREFIX + ".auto.deploy.topologies"; static final String DEFAULT_AUTO_DEPLOY_TOPOLOGIES = "manager,admin"; + static final String DISPATCH_HOST_WHITELIST = GATEWAY_CONFIG_FILE_PREFIX + ".dispatch.whitelist"; + static final String DISPATCH_HOST_WHITELIST_SERVICES = DISPATCH_HOST_WHITELIST + ".services"; + private static List<String> DEFAULT_GLOBAL_RULES_SERVICES; @@ -1089,4 +1092,22 @@ public class GatewayConfigImpl extends Configuration implements GatewayConfig { return topologyNames; } + public String getDispatchWhitelist() { + return get(DISPATCH_HOST_WHITELIST); + } + + @Override + public List<String> getDispatchWhitelistServices() { + List<String> result = new ArrayList<>(); + + String serviceList = get(DISPATCH_HOST_WHITELIST_SERVICES); + if (serviceList != null) { + for (String service : Arrays.asList(serviceList.split(","))) { + result.add(service.trim()); + } + } + + return result; + } + } http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java ---------------------------------------------------------------------- diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java index a103dac..2454e41 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java @@ -48,6 +48,7 @@ import org.apache.knox.gateway.services.security.token.TokenServiceException; import org.apache.knox.gateway.services.security.token.impl.JWT; import org.apache.knox.gateway.util.RegExUtils; import org.apache.knox.gateway.util.Urls; +import org.apache.knox.gateway.util.WhitelistUtils; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.MediaType.APPLICATION_XML; @@ -66,8 +67,6 @@ public class WebSSOResource { private static final String ORIGINAL_URL_REQUEST_PARAM = "originalUrl"; private static final String ORIGINAL_URL_COOKIE_NAME = "original-url"; private static final String DEFAULT_SSO_COOKIE_NAME = "hadoop-jwt"; - // default for the whitelist - open up for development - relative paths and localhost only - private static final String DEFAULT_WHITELIST = "^/.*$;^https?://(localhost|127.0.0.1|0:0:0:0:0:0:0:1|::1):\\d{0,9}/.*$"; private static final long TOKEN_TTL_DEFAULT = 30000L; static final String RESOURCE_PATH = "/api/v1/websso"; private static KnoxSSOMessages log = MessagesFactory.get( KnoxSSOMessages.class ); @@ -122,8 +121,7 @@ public class WebSSOResource { whitelist = context.getInitParameter(SSO_COOKIE_TOKEN_WHITELIST_PARAM); if (whitelist == null) { - // default to local/relative targets - whitelist = DEFAULT_WHITELIST; + whitelist = WhitelistUtils.getDispatchWhitelist(request); } String audiences = context.getInitParameter(SSO_COOKIE_TOKEN_AUDIENCES_PARAM); @@ -183,7 +181,8 @@ public class WebSSOResource { log.originalURLNotFound(); throw new WebApplicationException("Original URL not found in the request.", Response.Status.BAD_REQUEST); } - boolean validRedirect = RegExUtils.checkWhitelist(whitelist, original); + + boolean validRedirect = (whitelist == null) || whitelist.isEmpty() || RegExUtils.checkWhitelist(whitelist, original); if (!validRedirect) { log.whiteListMatchFail(original, whitelist); throw new WebApplicationException("Original URL not valid according to the configured whitelist.", http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java ---------------------------------------------------------------------- diff --git a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java index 0eb717e..65b3a26 100644 --- a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java +++ b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java @@ -17,11 +17,14 @@ */ package org.apache.knox.gateway.service.knoxsso; +import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.util.RegExUtils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import java.lang.reflect.Field; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; @@ -49,7 +52,6 @@ import org.apache.knox.gateway.services.security.token.JWTokenAuthority; import org.apache.knox.gateway.services.security.token.TokenServiceException; import org.apache.knox.gateway.services.security.token.impl.JWT; import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.apache.knox.gateway.util.RegExUtils; import org.easymock.EasyMock; import org.junit.Assert; import org.junit.BeforeClass; @@ -96,22 +98,22 @@ public class WebSSOResourceTest { Assert.assertTrue("Failed to match whitelist", RegExUtils.checkWhitelist(whitelist, "http://host.example2.com:1234/")); // fail on missing port - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example2.com/")); // fail on invalid port - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example.com:8081/")); // fail on alphanumeric port - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example.com:A080/")); // fail on invalid hostname/domain - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example.net:8080/")); // fail on required port - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example2.com/")); // fail on required https - Assert.assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, + assertFalse("Matched whitelist inappropriately", RegExUtils.checkWhitelist(whitelist, "http://host.example3.com/")); // match on localhost and port Assert.assertTrue("Failed to match whitelist", RegExUtils.checkWhitelist(whitelist, @@ -402,7 +404,6 @@ public class WebSSOResourceTest { @Test public void testCustomTTL() throws Exception { - ServletContext context = EasyMock.createNiceMock(ServletContext.class); EasyMock.expect(context.getInitParameter("knoxsso.cookie.name")).andReturn(null); EasyMock.expect(context.getInitParameter("knoxsso.cookie.secure.only")).andReturn(null); @@ -572,6 +573,145 @@ public class WebSSOResourceTest { assertTrue((expiresDate.getTime() - now.getTime()) < 30000L); } + @Test + public void testDefaultWhitelistLocalhostByAddress() throws Exception { + doTestDefaultLocalhostWhitelist("127.0.0.1"); + } + + @Test + public void testDefaultWhitelistLocalhostByName() throws Exception { + doTestDefaultLocalhostWhitelist("localhost"); + } + + @Test + public void testDefaultDomainWhitelist() throws Exception { + doTestDefaultDomainWhitelist("knox.test.org"); + doTestDefaultDomainWhitelist("knox.test.com"); + } + + private void doTestDefaultLocalhostWhitelist(String localhostId) throws Exception { + String whitelistValue = doTestDefaultWhitelist(localhostId); + assertTrue(whitelistValue.contains("localhost")); + } + + private void doTestDefaultDomainWhitelist(String hostname) throws Exception { + String whitelistValue = doTestDefaultWhitelist(hostname); + assertTrue(whitelistValue.contains(hostname.substring(hostname.indexOf('.')).replaceAll("\\.", "\\\\."))); + } + + + private String doTestDefaultWhitelist(String hostname) throws Exception { + final String testServiceRole = "TEST"; + + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getDispatchWhitelistServices()).andReturn(Collections.singletonList(testServiceRole)).anyTimes(); + EasyMock.expect(config.getDispatchWhitelist()).andReturn(null).anyTimes(); + EasyMock.replay(config); + + ServletContext context = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.name")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.secure.only")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.max.age")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.domain.suffix")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.redirect.whitelist.regex")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.token.audiences")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.token.ttl")).andReturn("60000"); + EasyMock.expect(context.getInitParameter("knoxsso.enable.session")).andReturn(null); + EasyMock.expect(context.getAttribute("org.apache.knox.gateway.config")).andReturn(config).anyTimes(); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getParameter("originalUrl")).andReturn("http://localhost:9080/service"); + EasyMock.expect(request.getParameterMap()).andReturn(Collections.<String,String[]>emptyMap()); + EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(request.getAttribute("targetServiceRole")).andReturn(testServiceRole).anyTimes(); + EasyMock.expect(request.getServerName()).andReturn(hostname).anyTimes(); + + Principal principal = EasyMock.createNiceMock(Principal.class); + EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); + EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); + + GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); + + JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); + EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); + + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class); + CookieResponseWrapper responseWrapper = new CookieResponseWrapper(response, outputStream); + + EasyMock.replay(principal, services, context, request); + + WebSSOResource webSSOResponse = new WebSSOResource(); + webSSOResponse.request = request; + webSSOResponse.response = responseWrapper; + webSSOResponse.context = context; + webSSOResponse.init(); + + Field whitelistField = webSSOResponse.getClass().getDeclaredField("whitelist"); + whitelistField.setAccessible(true); + String whitelistValue = (String) whitelistField.get(webSSOResponse); + assertNotNull(whitelistValue); + + return whitelistValue; + } + + @Test + public void testTopologyDefinedWhitelist() throws Exception { + final String testServiceRole = "TEST"; + + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getDispatchWhitelistServices()).andReturn(Collections.singletonList(testServiceRole)).anyTimes(); + EasyMock.expect(config.getDispatchWhitelist()).andReturn(null).anyTimes(); + EasyMock.replay(config); + + ServletContext context = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.name")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.secure.only")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.max.age")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.cookie.domain.suffix")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.redirect.whitelist.regex")).andReturn("^.*$"); + EasyMock.expect(context.getInitParameter("knoxsso.token.audiences")).andReturn(null); + EasyMock.expect(context.getInitParameter("knoxsso.token.ttl")).andReturn("60000"); + EasyMock.expect(context.getInitParameter("knoxsso.enable.session")).andReturn(null); + EasyMock.expect(context.getAttribute("org.apache.knox.gateway.config")).andReturn(config).anyTimes(); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getParameter("originalUrl")).andReturn("http://localhost:9080/service"); + EasyMock.expect(request.getParameterMap()).andReturn(Collections.<String,String[]>emptyMap()); + EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); + EasyMock.expect(request.getAttribute("targetServiceRole")).andReturn(testServiceRole).anyTimes(); + EasyMock.expect(request.getServerName()).andReturn("localhost").anyTimes(); + + Principal principal = EasyMock.createNiceMock(Principal.class); + EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); + EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); + + GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); + + JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); + EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); + + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class); + CookieResponseWrapper responseWrapper = new CookieResponseWrapper(response, outputStream); + + EasyMock.replay(principal, services, context, request); + + WebSSOResource webSSOResponse = new WebSSOResource(); + webSSOResponse.request = request; + webSSOResponse.response = responseWrapper; + webSSOResponse.context = context; + webSSOResponse.init(); + + Field whitelistField = webSSOResponse.getClass().getDeclaredField("whitelist"); + whitelistField.setAccessible(true); + String whitelistValue = (String) whitelistField.get(webSSOResponse); + assertNotNull(whitelistValue); + assertEquals("^.*$", whitelistValue); + } + /** * A wrapper for HttpServletResponseWrapper to store the cookies */ http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/main/java/org/apache/knox/gateway/SpiGatewayMessages.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/SpiGatewayMessages.java b/gateway-spi/src/main/java/org/apache/knox/gateway/SpiGatewayMessages.java index 38e81be..c0ced17 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/SpiGatewayMessages.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/SpiGatewayMessages.java @@ -81,4 +81,12 @@ public interface SpiGatewayMessages { @Message( level = MessageLevel.ERROR, text = "Error reading Kerberos login configuration {0} : {1}" ) void errorReadingKerberosLoginConfig(String fileName, @StackTrace(level=MessageLevel.ERROR) Exception e); + @Message( level = MessageLevel.INFO, + text = "Applying a derived dispatch whitelist because none is configured in gateway-site: {0}" ) + void derivedDispatchWhitelist(final String derivedWhitelist); + + @Message( level = MessageLevel.ERROR, + text = "The dispatch to {0} was disallowed because it fails the dispatch whitelist validation. See documentation for dispatch whitelisting." ) + void dispatchDisallowed(String uri); + } http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java index 8a43cb7..c5e2337 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java @@ -418,4 +418,20 @@ public interface GatewayConfig { */ List<String> getAutoDeployTopologyNames(); + /* + * Get the semicolon-delimited set of regular expressions defining to which hosts Knox will permit requests to be + * dispatched. + * + * @return The whitelist, which will be null if none is configured (in which case, requests to any host are permitted). + */ + String getDispatchWhitelist(); + + /** + * Get the set of service roles to which the dispatch whitelist will be applied. + * + * @return The service roles, or an empty list if none are configured. + */ + List<String> getDispatchWhitelistServices(); + + } http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilter.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilter.java b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilter.java index 8f3399a..0a993a0 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilter.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilter.java @@ -23,6 +23,8 @@ import org.apache.knox.gateway.config.ConfigurationInjectorBuilder; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.knox.gateway.util.RegExUtils; +import org.apache.knox.gateway.util.WhitelistUtils; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -110,19 +112,40 @@ public class GatewayDispatchFilter extends AbstractGatewayFilter { protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String method = request.getMethod().toUpperCase(); Adapter adapter = METHOD_ADAPTERS.get(method); - if ( adapter != null ) { - try { - adapter.doMethod(getDispatch(), request, response); - } catch ( URISyntaxException e ) { - throw new ServletException(e); + if (adapter != null) { + if (isDispatchAllowed(request)) { + try { + adapter.doMethod(getDispatch(), request, response); + } catch (URISyntaxException e) { + throw new ServletException(e); + } + } else { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); } } else { response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED); } } + private boolean isDispatchAllowed(HttpServletRequest request) { + boolean isAllowed = true; + + String whitelist = WhitelistUtils.getDispatchWhitelist(request); + if (whitelist != null) { + + String requestURI = request.getRequestURI(); + + isAllowed = RegExUtils.checkWhitelist(whitelist, requestURI); + if (!isAllowed) { + LOG.dispatchDisallowed(requestURI); + } + } + + return isAllowed; + } + private interface Adapter { - public void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response) + void doMethod(Dispatch dispatch, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, URISyntaxException; } http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/main/java/org/apache/knox/gateway/util/WhitelistUtils.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/util/WhitelistUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/util/WhitelistUtils.java new file mode 100644 index 0000000..50795e5 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/util/WhitelistUtils.java @@ -0,0 +1,91 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.knox.gateway.SpiGatewayMessages; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class WhitelistUtils { + + static final String LOCALHOST_REGEXP_SEGMENT = "(localhost|127\\.0\\.0\\.1|0:0:0:0:0:0:0:1|::1)"; + + static final String LOCALHOST_REGEXP = "^" + LOCALHOST_REGEXP_SEGMENT + "$"; + + static final String DEFAULT_DISPATCH_WHITELIST_TEMPLATE = "^/.*$;^https?://%s:[0-9]+/?.*$"; + + private static final SpiGatewayMessages LOG = MessagesFactory.get(SpiGatewayMessages.class); + + private static final List<String> DEFAULT_SERVICE_ROLES = Arrays.asList("KNOXSSO"); + + + public static String getDispatchWhitelist(HttpServletRequest request) { + String whitelist = null; + + GatewayConfig config = (GatewayConfig) request.getServletContext().getAttribute("org.apache.knox.gateway.config"); + if (config != null) { + List<String> whitelistedServiceRoles = new ArrayList<>(); + whitelistedServiceRoles.addAll(DEFAULT_SERVICE_ROLES); + whitelistedServiceRoles.addAll(config.getDispatchWhitelistServices()); + + String serviceRole = (String) request.getAttribute("targetServiceRole"); + if (whitelistedServiceRoles.contains(serviceRole)) { + // Check the whitelist against the URL to be dispatched + whitelist = config.getDispatchWhitelist(); + if (whitelist == null || whitelist.isEmpty()) { + whitelist = deriveDefaultDispatchWhitelist(request); + LOG.derivedDispatchWhitelist(whitelist); + } + } + } + + return whitelist; + } + + private static String deriveDefaultDispatchWhitelist(HttpServletRequest request) { + String defaultWhitelist = null; + + String thisHost = request.getHeader("X-Forwarded-Host"); + if (thisHost == null) { + thisHost = request.getServerName(); + } + + // If the host is not some form of localhost, try to determine its domain + if (!thisHost.matches(LOCALHOST_REGEXP)) { + int domainIndex = thisHost.indexOf('.'); + if (domainIndex > 0) { + String domain = thisHost.substring(thisHost.indexOf('.')); + String domainPattern = ".+" + domain.replaceAll("\\.", "\\\\."); + defaultWhitelist = String.format(DEFAULT_DISPATCH_WHITELIST_TEMPLATE, domainPattern); + } + } + + // If the host is localhost or the domain could not be determined, default to the local/relative whitelist + if (defaultWhitelist == null) { + defaultWhitelist = String.format(DEFAULT_DISPATCH_WHITELIST_TEMPLATE, LOCALHOST_REGEXP_SEGMENT); + } + + return defaultWhitelist; + } + + +} http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilterTest.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilterTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilterTest.java new file mode 100644 index 0000000..0408d79 --- /dev/null +++ b/gateway-spi/src/test/java/org/apache/knox/gateway/dispatch/GatewayDispatchFilterTest.java @@ -0,0 +1,263 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.dispatch; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.test.mock.MockHttpServletResponse; +import org.easymock.EasyMock; +import org.junit.Test; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + + +public class GatewayDispatchFilterTest { + + /** + * Verify that a whitelist violation results in a HTTP 400 response. + */ + @Test + public void testServiceDispatchWhitelistViolation() throws Exception { + final String serviceRole = "KNOXSSO"; + + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getDispatchWhitelistServices()).andReturn(Collections.emptyList()).anyTimes(); + EasyMock.expect(config.getDispatchWhitelist()).andReturn(null).anyTimes(); + EasyMock.replay(config); + + ServletContext sc = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(sc.getAttribute("org.apache.knox.gateway.config")).andReturn(config).anyTimes(); + EasyMock.replay(sc); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getMethod()).andReturn("GET").anyTimes(); + EasyMock.expect(request.getServerName()).andReturn("localhost").anyTimes(); + EasyMock.expect(request.getRequestURI()).andReturn("http://www.notonmylist.org:9999").anyTimes(); + EasyMock.expect(request.getAttribute("targetServiceRole")).andReturn(serviceRole).anyTimes(); + EasyMock.expect(request.getServletContext()).andReturn(sc).anyTimes(); + EasyMock.replay(request); + + HttpServletResponse response = new TestHttpServletResponse(); + (new GatewayDispatchFilter()).doFilter(request, response, null); + assertEquals(400, response.getStatus()); + } + + + /** + * If the dispatch service is not configured to honor the whitelist, the dispatching should be permitted. + */ + @Test + public void testServiceDispatchWhitelistNoServiceRoles() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.emptyList(), + "^https?://localhost.*$", + serviceRole, + "http://www.notonmylist.org:9999", true); + } + + /** + * If the dispatch service is configured to honor the whitelist, but no whitelist is configured, then the default + * whitelist should be applied. If the dispatch URL does not match the default whitelist, then the dispatch should be + * disallowed. + */ + @Test + public void testServiceDispatchWhitelistNoWhiteListForRole_invalid() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + null, + serviceRole, + "http://www.notonmylist.org:9999", false); + } + + /** + * If the dispatch service is configured to honor the whitelist, but no whitelist is configured, then the default + * whitelist should be applied. If the dispatch URL does not match the default whitelist, then the dispatch should be + * disallowed. + */ + @Test + public void testServiceDispatchWhitelistNoWhiteListForRole_invalid_alt_localhost() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "127.0.0.1", + null, + serviceRole, + "http://www.notonmylist.org:9999", false); + } + + /** + * If the dispatch service is configured to honor the whitelist, but no whitelist is configured, then the default + * whitelist should be applied. If the dispatch URL does not match the default domain-based whitelist, then the + * dispatch should be disallowed. + */ + @Test + public void testServiceDispatchWhitelistNoWhiteListForRole_invalid_domain() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "knoxbox.test.org", + null, + serviceRole, + "http://www.notonmylist.org:9999", false); + } + + /** + * If the dispatch service is configured to honor the whitelist, but no whitelist is configured, then the default + * whitelist should be applied. If the dispatch URL matches the default domain-based whitelist, then the dispatch + * should be permitted. + */ + @Test + public void testServiceDispatchWhitelistNoWhiteListForRole_valid_domain() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "knoxbox.test.org", + null, + serviceRole, + "http://onmylist.test.org:9999", true); + } + + /** + * If the dispatch service is configured to honor the whitelist, but no whitelist is configured, then the default + * whitelist should be applied. If the dispatch URL does match the default whitelist, then the dispatch should be + * allowed. + */ + @Test + public void testServiceDispatchWhitelistNoWhiteListForRole_valid() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + null, + serviceRole, + "http://localhost:9999", true); + } + + /** + * An empty whitelist should be treated as the default whitelist. + */ + @Test + public void testServiceDispatchWhitelistEmptyWhitelist() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "", + serviceRole, + "http://www.notonmylist.org:9999", + false); + } + + + /** + * If a custom whitelist is configured, and the requested service role is among those configured to honor that + * whitelist, the request should be disallowed if the URL does NOT match the whitelist. + */ + @Test + public void testServiceDispatchWhitelistCustomWhitelist_invalid() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "^.*mydomain\\.org.*$;^.*myotherdomain.com.*", + serviceRole, + "http://www.notonmylist.org:9999", + false); + } + + + /** + * If a custom whitelist is configured, and the requested service role is among those configured to honor that + * whitelist, the request should be permitted if the URL matches the whitelist. + */ + @Test + public void testServiceDispatchWhitelistCustomWhitelist_valid() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Collections.singletonList(serviceRole), + "^.*mydomain\\.org.*$;^.*myotherdomain.com.*;^.*onmylist\\.org.*$", + serviceRole, + "http://www.onmylist.org:9999", + true); + } + + /** + * The configured whitelist should be ignored for services which are NOT configured to honor the whitelist, and those + * dispatches should be permitted. + */ + @Test + public void testServiceDispatchWhitelistCustomWhitelistNoServiceRoles() throws Exception { + final String serviceRole = "TESTROLE"; + doTestServiceDispatchWhitelist(Arrays.asList("MYROLE","SOMEOTHER_ROLE"), // Different roles than the one requested + "^.*mydomain\\.org.*$;^.*myotherdomain.com.*", + serviceRole, + "http://www.onmylist.org:9999", + true); + } + + + private void doTestServiceDispatchWhitelist(List<String> whitelistedServices, + String whitelist, + String serviceRole, + String dispatchURL, + boolean expectation) throws Exception { + doTestServiceDispatchWhitelist(whitelistedServices, "localhost", whitelist, serviceRole, dispatchURL, expectation); + } + + private void doTestServiceDispatchWhitelist(List<String> whitelistedServices, + String serverName, + String whitelist, + String serviceRole, + String dispatchURL, + boolean expectation) throws Exception { + + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getDispatchWhitelistServices()).andReturn(whitelistedServices).anyTimes(); + EasyMock.expect(config.getDispatchWhitelist()).andReturn(whitelist).anyTimes(); + EasyMock.replay(config); + + ServletContext sc = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(sc.getAttribute("org.apache.knox.gateway.config")).andReturn(config).anyTimes(); + EasyMock.replay(sc); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getServerName()).andReturn(serverName).anyTimes(); + EasyMock.expect(request.getRequestURI()).andReturn(dispatchURL).anyTimes(); + EasyMock.expect(request.getAttribute("targetServiceRole")).andReturn(serviceRole).anyTimes(); + EasyMock.expect(request.getServletContext()).andReturn(sc).anyTimes(); + EasyMock.replay(request); + + GatewayDispatchFilter gdf = new GatewayDispatchFilter(); + Method isDispatchAllowedMethod = + GatewayDispatchFilter.class.getDeclaredMethod("isDispatchAllowed", HttpServletRequest.class); + isDispatchAllowedMethod.setAccessible(true); + boolean isAllowed = (boolean) isDispatchAllowedMethod.invoke(gdf, request); + assertEquals(expectation, isAllowed); + } + + private static class TestHttpServletResponse extends MockHttpServletResponse { + int status = 0; + + @Override + public void sendError(int i) throws IOException { + status = i; + } + + @Override + public int getStatus() { + return status; + } + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-spi/src/test/java/org/apache/knox/gateway/util/WhitelistUtilsTest.java ---------------------------------------------------------------------- diff --git a/gateway-spi/src/test/java/org/apache/knox/gateway/util/WhitelistUtilsTest.java b/gateway-spi/src/test/java/org/apache/knox/gateway/util/WhitelistUtilsTest.java new file mode 100644 index 0000000..3094c6f --- /dev/null +++ b/gateway-spi/src/test/java/org/apache/knox/gateway/util/WhitelistUtilsTest.java @@ -0,0 +1,145 @@ +/** + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.util; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.easymock.EasyMock; +import org.junit.Test; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class WhitelistUtilsTest { + + @Test + public void testDefault() throws Exception { + String whitelist = doTestGetDispatchWhitelist(createMockGatewayConfig(Collections.emptyList(), null), "TEST"); + assertNull("The test service role is not configured to honor the whitelist, so there should be none returned.", + whitelist); + } + + /** + * KNOXSSO is implicitly included in the set of service roles for which the whitelist will be applied. + */ + @Test + public void testDefaultKnoxSSO() throws Exception { + String whitelist = doTestGetDispatchWhitelist(createMockGatewayConfig(Collections.emptyList(), null), "KNOXSSO"); + assertNotNull(whitelist); + } + + @Test + public void testDefaultForAffectedServiceRole() throws Exception { + final String serviceRole = "TEST"; + + GatewayConfig config = createMockGatewayConfig(Collections.singletonList(serviceRole), null); + + // Check localhost by name + String whitelist = doTestGetDispatchWhitelist(config, serviceRole); + assertNotNull(whitelist); + assertTrue(whitelist.contains("localhost")); + + // Check localhost by loopback address + whitelist = doTestGetDispatchWhitelist(config, "127.0.0.1", serviceRole); + assertNotNull(whitelist); + assertTrue(whitelist.contains("localhost")); + } + + + @Test + public void testDefaultDomainWhitelist() throws Exception { + final String serviceRole = "TEST"; + + String whitelist = + doTestGetDispatchWhitelist(createMockGatewayConfig(Collections.singletonList(serviceRole), null), + "host0.test.org", + serviceRole); + assertNotNull(whitelist); + assertTrue(whitelist.contains("\\.test\\.org")); + } + + @Test + public void testDefaultProxiedDomainWhitelist() throws Exception { + final String serviceRole = "TEST"; + + String whitelist = + doTestGetDispatchWhitelist(createMockGatewayConfig(Collections.singletonList(serviceRole), null), + "host0.test.org", + "forwarded-host.proxy.org", + serviceRole); + assertNotNull(whitelist); + assertTrue(whitelist.contains("\\.proxy\\.org")); + } + + @Test + public void testConfiguredWhitelist() throws Exception { + final String serviceRole = "TEST"; + final String WHITELIST = "^.*\\.my\\.domain\\.com.*$"; + + String whitelist = + doTestGetDispatchWhitelist(createMockGatewayConfig(Collections.singletonList(serviceRole), WHITELIST), + serviceRole); + assertNotNull(whitelist); + assertTrue(whitelist.equals(WHITELIST)); + } + + + private String doTestGetDispatchWhitelist(GatewayConfig config, String serviceRole) { + return doTestGetDispatchWhitelist(config, "localhost", serviceRole); + } + + + private String doTestGetDispatchWhitelist(GatewayConfig config, + String serverName, + String serviceRole) { + return doTestGetDispatchWhitelist(config, serverName, null, serviceRole); + } + + private String doTestGetDispatchWhitelist(GatewayConfig config, + String serverName, + String xForwardedHost, + String serviceRole) { + ServletContext sc = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(sc.getAttribute("org.apache.knox.gateway.config")).andReturn(config).anyTimes(); + EasyMock.replay(sc); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getServerName()).andReturn(serverName).anyTimes(); + EasyMock.expect(request.getHeader("X-Forwarded-Host")).andReturn(xForwardedHost).anyTimes(); + EasyMock.expect(request.getAttribute("targetServiceRole")).andReturn(serviceRole).anyTimes(); + EasyMock.expect(request.getServletContext()).andReturn(sc).anyTimes(); + EasyMock.replay(request); + + return WhitelistUtils.getDispatchWhitelist(request); + } + + + private static GatewayConfig createMockGatewayConfig(final List<String> serviceRoles, final String whitelist) { + GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); + EasyMock.expect(config.getDispatchWhitelistServices()).andReturn(serviceRoles).anyTimes(); + EasyMock.expect(config.getDispatchWhitelist()).andReturn(whitelist).anyTimes(); + EasyMock.replay(config); + + return config; + } + +} http://git-wip-us.apache.org/repos/asf/knox/blob/c94d9b1e/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java ---------------------------------------------------------------------- diff --git a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java index ef5c1c4..f8d4ec7 100644 --- a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java +++ b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java @@ -723,4 +723,13 @@ public class GatewayTestConfig extends Configuration implements GatewayConfig { return null; } + public String getDispatchWhitelist() { + return null; + } + + @Override + public List<String> getDispatchWhitelistServices() { + return Collections.emptyList(); + } + }
