Josh, per our guidelines, comment should be
> [CALCITE-2294] Allow customization for AvaticaServerConfiguration for > plugging new authentication mechanisms (Karan Mehta) Can you force-push to fix please. > On May 30, 2018, at 4:07 PM, [email protected] wrote: > > 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 >
