osdir.com

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: calcite-avatica git commit: CALCITE-2294 Allow customization for AvaticaServerConfiguration for plugging new authentication mechanisms


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, elserj@xxxxxxxxxx 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 <elserj@xxxxxxxxxx>
> 
> 
> 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 <k.mehta@xxxxxxxxxxxxxx>
> Authored: Wed May 30 19:04:05 2018 -0400
> Committer: Josh Elser <elserj@xxxxxxxxxx>
> 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
>