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

Reply via email to