Repository: calcite-avatica Updated Branches: refs/heads/master 0638c6614 -> 3ab9ec6f8
CALCITE-2294 Allow customization for AvaticaServerConfiguration for plugging new authentication mechanisms Closes #48 Signed-off-by: Josh Elser <[email protected]> Project: http://git-wip-us.apache.org/repos/asf/calcite-avatica/repo Commit: http://git-wip-us.apache.org/repos/asf/calcite-avatica/commit/3ab9ec6f Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica/tree/3ab9ec6f Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica/diff/3ab9ec6f Branch: refs/heads/master Commit: 3ab9ec6f884607417d8e1badd69a681f958c2703 Parents: 0638c66 Author: Karan Mehta <[email protected]> Authored: Wed May 30 19:04:05 2018 -0400 Committer: Josh Elser <[email protected]> Committed: Wed May 30 19:04:25 2018 -0400 ---------------------------------------------------------------------- .../avatica/remote/AuthenticationType.java | 3 +- .../calcite/avatica/server/HttpServer.java | 180 ++++++---- .../server/CustomAuthHttpServerTest.java | 338 +++++++++++++++++++ .../calcite/avatica/server/HttpAuthBase.java | 15 + ...yStringParameterRemoteUserExtractorTest.java | 14 +- site/_docs/security.md | 18 + 6 files changed, 494 insertions(+), 74 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java b/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java index 2662e14..f483be9 100644 --- a/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java +++ b/core/src/main/java/org/apache/calcite/avatica/remote/AuthenticationType.java @@ -23,7 +23,8 @@ public enum AuthenticationType { NONE, BASIC, DIGEST, - SPNEGO; + SPNEGO, + CUSTOM; } // End AuthenticationType.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java ---------------------------------------------------------------------- diff --git a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java index 08f4274..f7b6e75 100644 --- a/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java +++ b/server/src/main/java/org/apache/calcite/avatica/server/HttpServer.java @@ -215,35 +215,64 @@ public class HttpServer { server = new Server(threadPool); server.manage(threadPool); - final ServerConnector connector = configureConnector(getConnector(), port); - ConstraintSecurityHandler securityHandler = null; + ServerConnector serverConnector = null; + HandlerList handlerList = null; + if (null != this.config && AuthenticationType.CUSTOM == config.getAuthenticationType()) { + if (null != handler || null != sslFactory) { + throw new IllegalStateException("Handlers and SSLFactory cannot be configured with " + + "the HTTPServer Builder when using CUSTOM Authentication Type."); + } + } else { + serverConnector = configureServerConnector(); + handlerList = configureHandlers(); + } - if (null != this.config) { - switch (config.getAuthenticationType()) { - case SPNEGO: - // Get the Handler for SPNEGO authentication - securityHandler = configureSpnego(server, connector, this.config); - break; - case BASIC: - securityHandler = configureBasicAuthentication(server, connector, config); - break; - case DIGEST: - securityHandler = configureDigestAuthentication(server, connector, config); - break; - default: - // Pass - break; + // Apply server customizers + for (ServerCustomizer<Server> customizer : this.serverCustomizers) { + LOG.info("Customizing server with customizer: " + customizer.getClass()); + customizer.customize(server); + } + + try { + server.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + if (null != serverConnector && null != handlerList) { + port = serverConnector.getLocalPort(); + LOG.info("Service listening on port {}.", getPort()); + + // Set the information about the address for this server + try { + this.handler.setServerRpcMetadata(createRpcServerMetadata(serverConnector)); + } catch (UnknownHostException e) { + // Failed to do the DNS lookup, bail out. + throw new RuntimeException(e); } + } else if (0 == server.getConnectors().length) { + String error = "No server connectors have been configured for this Avatica server"; + LOG.error(error); + throw new RuntimeException(error); } + } + private ServerConnector configureServerConnector() { + final ServerConnector connector = getServerConnector(); + connector.setIdleTimeout(60 * 1000); + connector.setSoLingerTime(-1); + connector.setPort(port); server.setConnectors(new Connector[] { connector }); + return connector; + } - // Default to using the handler that was passed in + private HandlerList configureHandlers() { final HandlerList handlerList = new HandlerList(); Handler avaticaHandler = handler; // Wrap the provided handler for security if we made one - if (null != securityHandler) { + if (null != config) { + ConstraintSecurityHandler securityHandler = getSecurityHandler(); securityHandler.setHandler(handler); avaticaHandler = securityHandler; } @@ -251,30 +280,30 @@ public class HttpServer { handlerList.setHandlers(new Handler[] {avaticaHandler, new DefaultHandler()}); server.setHandler(handlerList); - // Apply server customizers - for (ServerCustomizer<Server> customizer : this.serverCustomizers) { - customizer.customize(server); - } - - try { - server.start(); - } catch (Exception e) { - throw new RuntimeException(e); - } - port = connector.getLocalPort(); - - LOG.info("Service listening on port {}.", getPort()); + return handlerList; + } - // Set the information about the address for this server - try { - this.handler.setServerRpcMetadata(createRpcServerMetadata(connector)); - } catch (UnknownHostException e) { - // Failed to do the DNS lookup, bail out. - throw new RuntimeException(e); - } + private ConstraintSecurityHandler getSecurityHandler() { + ConstraintSecurityHandler securityHandler = null; + switch (config.getAuthenticationType()) { + case SPNEGO: + // Get the Handler for SPNEGO authentication + securityHandler = configureSpnego(server, this.config); + break; + case BASIC: + securityHandler = configureBasicAuthentication(server, config); + break; + case DIGEST: + securityHandler = configureDigestAuthentication(server, config); + break; + default: + // Pass + break; + } + return securityHandler; } - private ServerConnector getConnector() { + protected ServerConnector getServerConnector() { HttpConnectionFactory factory = new HttpConnectionFactory(); factory.getHttpConfiguration().setRequestHeaderSize(maxAllowedHeaderSize); @@ -302,10 +331,9 @@ public class HttpServer { /** * Configures the <code>connector</code> given the <code>config</code> for using SPNEGO. * - * @param connector The connector to configure * @param config The configuration */ - protected ConstraintSecurityHandler configureSpnego(Server server, ServerConnector connector, + protected ConstraintSecurityHandler configureSpnego(Server server, AvaticaServerConfiguration config) { final String realm = Objects.requireNonNull(config.getKerberosRealm()); final String principal = Objects.requireNonNull(config.getKerberosPrincipal()); @@ -318,7 +346,7 @@ public class HttpServer { // Roles are "realms" for Kerberos/SPNEGO final String[] allowedRealms = getAllowedRealms(realm, config); - return configureCommonAuthentication(server, connector, config, Constraint.__SPNEGO_AUTH, + return configureCommonAuthentication(Constraint.__SPNEGO_AUTH, allowedRealms, new AvaticaSpnegoAuthenticator(), realm, spnegoLoginService); } @@ -336,7 +364,7 @@ public class HttpServer { } protected ConstraintSecurityHandler configureBasicAuthentication(Server server, - ServerConnector connector, AvaticaServerConfiguration config) { + AvaticaServerConfiguration config) { final String[] allowedRoles = config.getAllowedRoles(); final String realm = config.getHashLoginServiceRealm(); final String loginServiceProperties = config.getHashLoginServiceProperties(); @@ -344,12 +372,12 @@ public class HttpServer { HashLoginService loginService = new HashLoginService(realm, loginServiceProperties); server.addBean(loginService); - return configureCommonAuthentication(server, connector, config, Constraint.__BASIC_AUTH, + return configureCommonAuthentication(Constraint.__BASIC_AUTH, allowedRoles, new BasicAuthenticator(), null, loginService); } protected ConstraintSecurityHandler configureDigestAuthentication(Server server, - ServerConnector connector, AvaticaServerConfiguration config) { + AvaticaServerConfiguration config) { final String[] allowedRoles = config.getAllowedRoles(); final String realm = config.getHashLoginServiceRealm(); final String loginServiceProperties = config.getHashLoginServiceProperties(); @@ -357,12 +385,11 @@ public class HttpServer { HashLoginService loginService = new HashLoginService(realm, loginServiceProperties); server.addBean(loginService); - return configureCommonAuthentication(server, connector, config, Constraint.__DIGEST_AUTH, + return configureCommonAuthentication(Constraint.__DIGEST_AUTH, allowedRoles, new DigestAuthenticator(), null, loginService); } - protected ConstraintSecurityHandler configureCommonAuthentication(Server server, - ServerConnector connector, AvaticaServerConfiguration config, String constraintName, + protected ConstraintSecurityHandler configureCommonAuthentication(String constraintName, String[] allowedRoles, Authenticator authenticator, String realm, LoginService loginService) { @@ -467,6 +494,8 @@ public class HttpServer { // The maximum size in bytes of an http header the server will read (64KB) private int maxAllowedHeaderSize = MAX_ALLOWED_HEADER_SIZE; + private AvaticaServerConfiguration serverConfig; + private Subject subject; public Builder() {} @@ -659,6 +688,22 @@ public class HttpServer { return withAuthentication(AuthenticationType.DIGEST, properties, allowedRoles); } + /** + * Configures the server to use CUSTOM authentication mechanism, which can allow users to + * combine benefits of multiple auth methods. See <code>CustomAuthHttpServerTest</code> for + * examples on how to use it. + * Note: Default ServerConnectors and Handlers will NOT be used. + * Customize them directly using instances <code>{@link ServerCustomizer}</code> + * @param config AvaticaServerConfiguration implementation that configures various details + * about the authentication mechanism for <code>{@link HttpServer}</code> + * @return <code>this</code> + */ + public Builder<T> withCustomAuthentication(AvaticaServerConfiguration config) { + this.authenticationType = AuthenticationType.CUSTOM; + this.serverConfig = config; + return this; + } + private Builder<T> withAuthentication(AuthenticationType authType, String properties, String[] allowedRoles) { this.loginServiceRealm = "Avatica"; @@ -721,18 +766,18 @@ public class HttpServer { */ @SuppressWarnings("unchecked") public HttpServer build() { - final AvaticaServerConfiguration serverConfig; - final Subject subject; switch (authenticationType) { case NONE: serverConfig = null; subject = null; + handler = buildHandler(this, serverConfig); break; case BASIC: case DIGEST: // Build the configuration for BASIC or DIGEST authentication. serverConfig = buildUserAuthenticationConfiguration(this); subject = null; + handler = buildHandler(this, serverConfig); break; case SPNEGO: if (usingTLS) { @@ -746,20 +791,19 @@ public class HttpServer { subject = null; } serverConfig = buildSpnegoConfiguration(this); + handler = buildHandler(this, serverConfig); + break; + case CUSTOM: + // We don't need to build any Config here since + // serverConfig is already assigned the required AvaticaServerConfiguration + serverConfig = buildCustomConfiguration(this); + subject = null; break; default: throw new IllegalArgumentException("Unhandled AuthenticationType"); } - AvaticaHandler handler = buildHandler(this, serverConfig); - SslContextFactory sslFactory = null; - if (usingTLS) { - sslFactory = new SslContextFactory(); - sslFactory.setKeyStorePath(this.keystore.getAbsolutePath()); - sslFactory.setKeyStorePassword(keystorePassword); - sslFactory.setTrustStorePath(truststore.getAbsolutePath()); - sslFactory.setTrustStorePassword(truststorePassword); - } + SslContextFactory sslFactory = buildSSLContextFactory(); List<ServerCustomizer<Server>> jettyCustomizers = new ArrayList<>(); for (ServerCustomizer<?> customizer : this.serverCustomizers) { @@ -771,6 +815,22 @@ public class HttpServer { maxAllowedHeaderSize); } + protected SslContextFactory buildSSLContextFactory() { + SslContextFactory sslFactory = null; + if (usingTLS) { + sslFactory = new SslContextFactory(); + sslFactory.setKeyStorePath(this.keystore.getAbsolutePath()); + sslFactory.setKeyStorePassword(keystorePassword); + sslFactory.setTrustStorePath(truststore.getAbsolutePath()); + sslFactory.setTrustStorePassword(truststorePassword); + } + return sslFactory; + } + + private AvaticaServerConfiguration buildCustomConfiguration(Builder<T> tBuilder) { + return tBuilder.serverConfig; + } + /** * Creates the appropriate {@link AvaticaHandler}. * http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java b/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java new file mode 100644 index 0000000..dbb2f4c --- /dev/null +++ b/server/src/test/java/org/apache/calcite/avatica/server/CustomAuthHttpServerTest.java @@ -0,0 +1,338 @@ +/* + * 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.calcite.avatica.server; + +import org.apache.calcite.avatica.ConnectionSpec; +import org.apache.calcite.avatica.jdbc.JdbcMeta; +import org.apache.calcite.avatica.remote.AuthenticationType; +import org.apache.calcite.avatica.remote.Driver; +import org.apache.calcite.avatica.remote.LocalService; + +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.handler.DefaultHandler; +import org.eclipse.jetty.server.handler.HandlerList; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.SQLException; +import javax.servlet.http.HttpServletRequest; + +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.Properties; +import java.util.concurrent.Callable; + +/** + * Test class for providing CustomAvaticaServerConfiguration to the HTTP Server + */ +public class CustomAuthHttpServerTest extends HttpAuthBase { + private static final ConnectionSpec CONNECTION_SPEC = ConnectionSpec.HSQLDB; + private static HttpServer server; + private static String url; + + // Counters to keep track of number of function calls + private static int methodCallCounter1 = 0; + private static int methodCallCounter2 = 0; + private static int methodCallCounter3 = 0; + + @Before + public void before() { + methodCallCounter1 = 0; + methodCallCounter2 = 0; + methodCallCounter3 = 0; + } + + @After + public void stopServer() { + if (null != server) { + server.stop(); + } + } + + @Test + public void testCustomImpersonationConfig() throws Exception { + AvaticaServerConfiguration configuration = new CustomImpersonationConfig(); + createServer(configuration, false); + + readWriteData(url, "CUSTOM_CONFIG_1_TABLE", new Properties()); + Assert.assertEquals("supportsImpersonation should be called same number of " + + "times as doAsRemoteUser method", methodCallCounter1, methodCallCounter2); + Assert.assertEquals("supportsImpersonation should be called same number of " + + "times as getRemoteUserExtractor method", methodCallCounter1, methodCallCounter3); + } + + @Test + public void testCustomBasicImpersonationConfigWithAllowedUser() throws Exception { + AvaticaServerConfiguration configuration = new CustomBasicImpersonationConfig(); + createServer(configuration, true); + + final Properties props = new Properties(); + props.put("avatica_user", "USER2"); + props.put("avatica_password", "password2"); + props.put("user", "USER2"); + props.put("password", "password2"); + + readWriteData(url, "CUSTOM_CONFIG_2_ALLOWED_TABLE", props); + Assert.assertEquals("supportsImpersonation should be called same number of " + + "times as doAsRemoteUser method", methodCallCounter1, methodCallCounter2); + Assert.assertEquals("supportsImpersonation should be called same number of " + + "times as getRemoteUserExtractor method", methodCallCounter1, methodCallCounter3); + } + + @Test + public void testCustomBasicImpersonationConfigWithDisallowedUser() throws Exception { + AvaticaServerConfiguration configuration = new CustomBasicImpersonationConfig(); + createServer(configuration, true); + + final Properties props = new Properties(); + props.put("avatica_user", "USER1"); + props.put("avatica_password", "password1"); + props.put("user", "USER1"); + props.put("password", "password1"); + + try { + readWriteData(url, "CUSTOM_CONFIG_2_DISALLOWED_TABLE", props); + fail("Expected an exception"); + } catch (RuntimeException e) { + assertThat(e.getMessage(), containsString("Failed to execute HTTP Request, got HTTP/403")); + } + } + + @Test(expected = IllegalStateException.class) + public void testCustomConfigDisallowsWithHandlerMethod() { + AvaticaServerConfiguration configuration = new CustomBasicImpersonationConfig(); + server = new HttpServer.Builder() + .withCustomAuthentication(configuration) + .withHandler(Mockito.mock(AvaticaHandler.class)) + .withPort(0) + .build(); + server.start(); + } + + public static HttpServer getAvaticaServer() { + return server; + } + + @SuppressWarnings("unchecked") // needed for the mocked customizers, not the builder + protected void createServer(AvaticaServerConfiguration config, boolean isBasicAuth) + throws SQLException { + final JdbcMeta jdbcMeta = new JdbcMeta(CONNECTION_SPEC.url, + CONNECTION_SPEC.username, CONNECTION_SPEC.password); + LocalService service = new LocalService(jdbcMeta); + + ConnectorCustomizer connectorCustomizer = new ConnectorCustomizer(); + BasicAuthHandlerCustomizer basicAuthCustomizer = + new BasicAuthHandlerCustomizer(config, service, isBasicAuth); + + server = new HttpServer.Builder() + .withCustomAuthentication(config) + .withPort(0) + .withServerCustomizers( + Arrays.asList(connectorCustomizer, basicAuthCustomizer), Server.class) + .build(); + server.start(); + + // Create and grant permissions to our users + createHsqldbUsers(); + url = "jdbc:avatica:remote:url=http://localhost:" + connectorCustomizer.getLocalPort() + + ";authentication=BASIC;serialization=PROTOBUF"; + } + + /** + * Customizer to add ServerConnectors to the server + */ + static class ConnectorCustomizer implements ServerCustomizer<Server> { + + ServerConnector connector; + + @Override public void customize(Server server) { + HttpServer avaticaServer = getAvaticaServer(); + connector = avaticaServer.configureConnector(avaticaServer.getServerConnector(), 0); + server.setConnectors(new Connector[] { connector }); + } + + public int getLocalPort() { + return connector.getLocalPort(); + } + + } + + /** + * Customizer to add handlers to the server (with or without BasicAuth) + */ + static class BasicAuthHandlerCustomizer implements ServerCustomizer<Server> { + + AvaticaServerConfiguration configuration; + LocalService service; + boolean isBasicAuth; + + public BasicAuthHandlerCustomizer(AvaticaServerConfiguration configuration + , LocalService service, boolean isBasicAuth) { + this.configuration = configuration; + this.service = service; + this.isBasicAuth = isBasicAuth; + } + + @Override public void customize(Server server) { + HttpServer avaticaServer = getAvaticaServer(); + + HandlerFactory factory = new HandlerFactory(); + Handler avaticaHandler = factory.getHandler(service, + Driver.Serialization.PROTOBUF, null, configuration); + + if (isBasicAuth) { + ConstraintSecurityHandler securityHandler = + avaticaServer.configureBasicAuthentication(server, configuration); + securityHandler.setHandler(avaticaHandler); + avaticaHandler = securityHandler; + } + + HandlerList handlerList = new HandlerList(); + handlerList.setHandlers(new Handler[] { avaticaHandler, new DefaultHandler()}); + server.setHandler(handlerList); + } + } + + /** + * CustomImpersonationConfig doesn't authenticates the user but supports user impersonation + */ + static class CustomImpersonationConfig implements AvaticaServerConfiguration { + + + @Override public AuthenticationType getAuthenticationType() { + return AuthenticationType.CUSTOM; + } + + @Override public String getKerberosRealm() { + return null; + } + + @Override public String getKerberosPrincipal() { + return null; + } + + @Override public String[] getAllowedRoles() { + return new String[0]; + } + + @Override public String getHashLoginServiceRealm() { + return null; + } + + @Override public String getHashLoginServiceProperties() { + return null; + } + + @Override public boolean supportsImpersonation() { + methodCallCounter1++; + return true; + } + + @Override public <T> T doAsRemoteUser(String remoteUserName, + String remoteAddress, Callable<T> action) throws Exception { + methodCallCounter2++; + return action.call(); + } + @Override public RemoteUserExtractor getRemoteUserExtractor() { + return new RemoteUserExtractor() { + @Override public String extract(HttpServletRequest request) { + methodCallCounter3++; + return "randomUser"; + } + }; + } + + } + + /** + * CustomBasicImpersonationConfig supports BasicAuthentication with user impersonation + */ + static class CustomBasicImpersonationConfig implements AvaticaServerConfiguration { + + + @Override public AuthenticationType getAuthenticationType() { + return AuthenticationType.CUSTOM; + } + + @Override public String getKerberosRealm() { + return null; + } + + @Override public String getKerberosPrincipal() { + return null; + } + + @Override public String[] getAllowedRoles() { + return new String[] { "users" }; + } + + @Override public String getHashLoginServiceRealm() { + return "Avatica"; + } + + @Override public String getHashLoginServiceProperties() { + return HttpAuthBase.getHashLoginServicePropertiesString(); + } + + @Override public boolean supportsImpersonation() { + methodCallCounter1++; + return true; + } + + @Override public <T> T doAsRemoteUser(String remoteUserName, + String remoteAddress, Callable<T> action) throws Exception { + methodCallCounter2++; + if (remoteUserName.equals("USER1")) { + throw new RemoteUserDisallowedException("USER1 is a disallowed user!"); + } + return action.call(); + } + @Override public RemoteUserExtractor getRemoteUserExtractor() { + return new RemoteUserExtractor() { + @Override public String extract(HttpServletRequest request) + throws RemoteUserExtractionException { + methodCallCounter3++; + if (request instanceof Request) { + Authentication authentication = ((Request) request).getAuthentication(); + if (authentication instanceof UserAuthentication) { + UserIdentity userIdentity = ((UserAuthentication) authentication).getUserIdentity(); + return userIdentity.getUserPrincipal().getName(); + } + } + throw new RemoteUserExtractionException("Request doesn't contain user credentials."); + } + }; + } + } + +} + +// End CustomAuthHttpServerTest.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/server/src/test/java/org/apache/calcite/avatica/server/HttpAuthBase.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/calcite/avatica/server/HttpAuthBase.java b/server/src/test/java/org/apache/calcite/avatica/server/HttpAuthBase.java index cfaf302..6ce0afe 100644 --- a/server/src/test/java/org/apache/calcite/avatica/server/HttpAuthBase.java +++ b/server/src/test/java/org/apache/calcite/avatica/server/HttpAuthBase.java @@ -18,6 +18,8 @@ package org.apache.calcite.avatica.server; import org.apache.calcite.avatica.ConnectionSpec; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; @@ -75,6 +77,19 @@ public class HttpAuthBase { assertEquals(3, results.getInt(1)); } } + + static String getHashLoginServicePropertiesString() { + try { + final String userPropertiesFile = + URLDecoder.decode(HttpQueryStringParameterRemoteUserExtractorTest.class + .getResource("/auth-users.properties").getFile(), "UTF-8"); + assertNotNull("Could not find properties file for basic auth users", userPropertiesFile); + return userPropertiesFile; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + } // End HttpAuthBase.java http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java ---------------------------------------------------------------------- diff --git a/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java b/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java index 7fcde9a..33d91bc 100644 --- a/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java +++ b/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java @@ -29,14 +29,11 @@ import org.junit.runners.Parameterized.Parameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.util.List; import java.util.Properties; import java.util.concurrent.Callable; import static org.hamcrest.core.StringContains.containsString; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -129,16 +126,7 @@ public class HttpQueryStringParameterRemoteUserExtractorTest extends HttpAuthBas } @Override public String getHashLoginServiceProperties() { - try { - final String userPropertiesFile = - URLDecoder.decode(HttpQueryStringParameterRemoteUserExtractorTest.class - .getResource("/auth-users.properties").getFile(), "UTF-8"); - assertNotNull("Could not find properties file for basic auth users", userPropertiesFile); - return userPropertiesFile; - } catch (UnsupportedEncodingException e) { - LOG.error("Failed to decode path to Jetty users file", e); - throw new RuntimeException(e); - } + return HttpAuthBase.getHashLoginServicePropertiesString(); } }; http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/3ab9ec6f/site/_docs/security.md ---------------------------------------------------------------------- diff --git a/site/_docs/security.md b/site/_docs/security.md index 53233bd..a6909dc 100644 --- a/site/_docs/security.md +++ b/site/_docs/security.md @@ -7,6 +7,7 @@ auth_types: - { name: "HTTP Basic", anchor: "http-basic-authentication" } - { name: "HTTP Digest", anchor: "http-digest-authentication" } - { name: "Kerberos with SPNEGO", anchor: "kerberos-with-spnego-authentication" } + - { name: "Custom Authentication", anchor: "custom-authentication" } - { name: "Client implementation", anchor: "client-implementation" } --- <!-- @@ -256,6 +257,23 @@ config = new AvaticaServerConfiguration() { }; {% endhighlight %} +## Custom Authentication + +Avatica server now offers users to plugin their Custom Authentication mechanism through the HTTPServer Builder. +This is useful if users want to combine features of various authentication types. Examples include combining +basic authentication with impersonation or adding mutual authentication with impersonation. More Examples +are available in `CustomAuthHttpServerTest` class. + +Note: Users need to configure their own `ServerConnectors` and `Handlers` with the help of `ServerCustomizers`. +{% highlight java %} +AvaticaServerConfiguration configuration = new ExampleAvaticaServerConfiguration(); +HttpServer server = new HttpServer.Builder() + .withCustomAuthentication(configuration) + .withPort(8765) + .build(); +{% endhighlight %} + + ## Client implementation Many HTTP client libraries, such as [Apache Commons HttpComponents](https://hc.apache.org/), already have
