Repository: calcite-avatica
Updated Branches:
  refs/heads/master d9809ceef -> 4db1fb9c6


[CALCITE-1539] Enable proxy access to Avatica server for third party on behalf 
of end users

Contributions from elserj too:
* Set proper HTTP status codes on failure
* Makes the new test parameterized for each serialization type

Closes #8

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/4db1fb9c
Tree: http://git-wip-us.apache.org/repos/asf/calcite-avatica/tree/4db1fb9c
Diff: http://git-wip-us.apache.org/repos/asf/calcite-avatica/diff/4db1fb9c

Branch: refs/heads/master
Commit: 4db1fb9c66db8ccebc9e96ce678154ec69c557f0
Parents: d9809ce
Author: shiwang <[email protected]>
Authored: Tue Apr 11 15:40:26 2017 -0700
Committer: Josh Elser <[email protected]>
Committed: Tue Apr 25 18:50:59 2017 -0400

----------------------------------------------------------------------
 .../calcite/avatica/remote/AbstractHandler.java |  23 ++-
 .../apache/calcite/avatica/remote/Handler.java  |   2 +
 .../avatica/server/AvaticaJsonHandler.java      |   9 +-
 .../avatica/server/AvaticaProtobufHandler.java  |  11 +-
 .../server/AvaticaServerConfiguration.java      |   9 +
 ...QueryStringParameterRemoteUserExtractor.java |  44 +++++
 .../server/HttpRequestRemoteUserExtractor.java  |  36 ++++
 .../calcite/avatica/server/HttpServer.java      |  25 +++
 .../server/RemoteUserDisallowedException.java   |  34 ++++
 .../server/RemoteUserExtractionException.java   |  40 +++++
 .../avatica/server/RemoteUserExtractor.java     |  28 +++
 .../avatica/remote/AvaticaServersForTest.java   |  50 +++++-
 ...yStringParameterRemoteUserExtractorTest.java | 177 +++++++++++++++++++
 site/_docs/security.md                          |  26 +++
 14 files changed, 503 insertions(+), 11 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
----------------------------------------------------------------------
diff --git 
a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
index c37e063..b291d4c 100644
--- a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
+++ b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
@@ -47,7 +47,6 @@ public abstract class AbstractHandler<T> implements 
Handler<T> {
    *
    * @param response The {@link Response} to serialize.
    * @return A serialized representation of the {@link Response}.
-   * @throws IOException
    */
   abstract T encode(Response response) throws IOException;
 
@@ -107,10 +106,27 @@ public abstract class AbstractHandler<T> implements 
Handler<T> {
    * @return A HandlerResponse instance.
    */
   public HandlerResponse<T> convertToErrorResponse(Exception e) {
-    ErrorResponse errorResp = unwrapException(e);
+    return createErrorResponse(e, HTTP_INTERNAL_SERVER_ERROR);
+  }
+
+  /**
+   * Attempts to convert an Exception to an ErrorResponse with an HTTP status 
code of {@code 401}.
+   */
+  public HandlerResponse<T> unauthenticatedErrorResponse(Exception e) {
+    return createErrorResponse(e, HTTP_UNAUTHENTICATED);
+  }
 
+  /**
+   * Attempts to convert an Exception to an ErrorResponse with an HTTP stauts 
code of {@code 403}.
+   */
+  public HandlerResponse<T> unauthorizedErrorResponse(Exception e) {
+    return createErrorResponse(e, HTTP_UNAUTHORIZED);
+  }
+
+  private HandlerResponse<T> createErrorResponse(Exception e, int statusCode) {
+    ErrorResponse errorResp = unwrapException(e);
     try {
-      return new HandlerResponse<>(encode(errorResp), 
HTTP_INTERNAL_SERVER_ERROR);
+      return new HandlerResponse<>(encode(errorResp), statusCode);
     } catch (IOException e1) {
       // TODO provide a canned ErrorResponse
 
@@ -119,7 +135,6 @@ public abstract class AbstractHandler<T> implements 
Handler<T> {
       if (e instanceof RuntimeException) {
         throw (RuntimeException) e;
       }
-
       throw new RuntimeException(e);
     }
   }

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/core/src/main/java/org/apache/calcite/avatica/remote/Handler.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/Handler.java 
b/core/src/main/java/org/apache/calcite/avatica/remote/Handler.java
index 30d026c..5d8a6a8 100644
--- a/core/src/main/java/org/apache/calcite/avatica/remote/Handler.java
+++ b/core/src/main/java/org/apache/calcite/avatica/remote/Handler.java
@@ -27,6 +27,8 @@ import java.util.Objects;
  */
 public interface Handler<T> {
   int HTTP_OK = 200;
+  int HTTP_UNAUTHENTICATED = 401;
+  int HTTP_UNAUTHORIZED = 403;
   int HTTP_INTERNAL_SERVER_ERROR = 500;
   String HANDLER_SERIALIZATION_METRICS_NAME = "Handler.Serialization";
 

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/AvaticaJsonHandler.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaJsonHandler.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaJsonHandler.java
index b639183..15d2d3c 100644
--- 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaJsonHandler.java
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaJsonHandler.java
@@ -119,7 +119,8 @@ public class AvaticaJsonHandler extends 
AbstractAvaticaHandler {
         HandlerResponse<String> jsonResponse;
         try {
           if (null != serverConfig && serverConfig.supportsImpersonation()) {
-            jsonResponse = serverConfig.doAsRemoteUser(request.getRemoteUser(),
+            String remoteUser = 
serverConfig.getRemoteUserExtractor().extract(request);
+            jsonResponse = serverConfig.doAsRemoteUser(remoteUser,
                 request.getRemoteAddr(), new 
Callable<HandlerResponse<String>>() {
                   @Override public HandlerResponse<String> call() {
                     return jsonHandler.apply(jsonRequest);
@@ -128,6 +129,12 @@ public class AvaticaJsonHandler extends 
AbstractAvaticaHandler {
           } else {
             jsonResponse = jsonHandler.apply(jsonRequest);
           }
+        } catch (RemoteUserExtractionException e) {
+          LOG.debug("Failed to extract remote user from request", e);
+          jsonResponse = jsonHandler.unauthenticatedErrorResponse(e);
+        } catch (RemoteUserDisallowedException e) {
+          LOG.debug("Remote user is not authorized", e);
+          jsonResponse = jsonHandler.unauthorizedErrorResponse(e);
         } catch (Exception e) {
           LOG.debug("Error invoking request from {}", 
baseRequest.getRemoteAddr(), e);
           jsonResponse = jsonHandler.convertToErrorResponse(e);

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/AvaticaProtobufHandler.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaProtobufHandler.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaProtobufHandler.java
index f8723f1..cfa19c1 100644
--- 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaProtobufHandler.java
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaProtobufHandler.java
@@ -113,8 +113,11 @@ public class AvaticaProtobufHandler extends 
AbstractAvaticaHandler {
         HandlerResponse<byte[]> handlerResponse;
         try {
           if (null != serverConfig && serverConfig.supportsImpersonation()) {
+            // If we can't extract a user, need to throw 401 in that case.
+            String remoteUser = 
serverConfig.getRemoteUserExtractor().extract(request);
             // Invoke the ProtobufHandler inside as doAs for the remote user.
-            handlerResponse = 
serverConfig.doAsRemoteUser(request.getRemoteUser(),
+            // The doAsRemoteUser call may disallow a user, need to throw 403 
in that case.
+            handlerResponse = serverConfig.doAsRemoteUser(remoteUser,
               request.getRemoteAddr(), new Callable<HandlerResponse<byte[]>>() 
{
                 @Override public HandlerResponse<byte[]> call() {
                   return pbHandler.apply(requestBytes);
@@ -123,6 +126,12 @@ public class AvaticaProtobufHandler extends 
AbstractAvaticaHandler {
           } else {
             handlerResponse = pbHandler.apply(requestBytes);
           }
+        } catch (RemoteUserExtractionException e) {
+          LOG.debug("Failed to extract remote user from request", e);
+          handlerResponse = pbHandler.unauthenticatedErrorResponse(e);
+        } catch (RemoteUserDisallowedException e) {
+          LOG.debug("Remote user is not authorized", e);
+          handlerResponse = pbHandler.unauthorizedErrorResponse(e);
         } catch (Exception e) {
           LOG.debug("Error invoking request from {}", 
baseRequest.getRemoteAddr(), e);
           // Catch at the highest level of exceptions

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
index dd843a4..56860ec 100644
--- 
a/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/AvaticaServerConfiguration.java
@@ -89,9 +89,18 @@ public interface AvaticaServerConfiguration {
    * @param remoteUserName The remote user making a request to the Avatica 
server.
    * @param remoteAddress The address the remote user is making the request 
from.
    * @return The result from the Callable.
+   *
+   * @see RemoteUserDisallowedException to deny a remoteUser access
    */
   <T> T doAsRemoteUser(String remoteUserName, String remoteAddress, 
Callable<T> action)
       throws Exception;
+
+  /**
+   * Extract the user this request should execute as.
+   *
+   * @return Name of the RemoteUserExtractor
+   */
+  RemoteUserExtractor getRemoteUserExtractor();
 }
 
 // End AvaticaServerConfiguration.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractor.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractor.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractor.java
new file mode 100644
index 0000000..5419a2a
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractor.java
@@ -0,0 +1,44 @@
+/*
+ * 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 java.util.Objects;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * A {@link RemoteUserExtractor} that extracts the remote user from an HTTP 
query string parameter.
+ */
+public class HttpQueryStringParameterRemoteUserExtractor implements 
RemoteUserExtractor {
+  private final String parameter;
+
+  public HttpQueryStringParameterRemoteUserExtractor(String parameter) {
+    this.parameter = Objects.requireNonNull(parameter);
+  }
+
+  @Override public String extract(HttpServletRequest request) throws 
RemoteUserExtractionException {
+    final String remoteUser = request.getParameter(parameter);
+    if (remoteUser == null) {
+      throw new RemoteUserExtractionException(
+          "Failed to extract user from HTTP query string parameter: " + 
parameter);
+    }
+    return remoteUser;
+  }
+
+}
+
+// End HttpQueryStringParameterRemoteUserExtractor.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/HttpRequestRemoteUserExtractor.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/HttpRequestRemoteUserExtractor.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/HttpRequestRemoteUserExtractor.java
new file mode 100644
index 0000000..3a32faa
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/HttpRequestRemoteUserExtractor.java
@@ -0,0 +1,36 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+
+/**
+ * A {@link RemoteUserExtractor} that extracts the remote user from the HTTP 
request.
+ */
+public class HttpRequestRemoteUserExtractor implements RemoteUserExtractor {
+  @Override public String extract(HttpServletRequest request) throws 
RemoteUserExtractionException {
+    final String remoteUser = request.getRemoteUser();
+    if (remoteUser == null) {
+      throw new RemoteUserExtractionException(
+          "Failed to extract the remote user from the HTTP request");
+    }
+    return remoteUser;
+  }
+
+}
+
+// End HttpRequestRemoteUserExtractor.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/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 4c871f0..ffb67ca 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
@@ -403,6 +403,7 @@ public class HttpServer {
     private File keytab;
 
     private DoAsRemoteUserCallback remoteUserCallback;
+    private RemoteUserExtractor remoteUserExtractor = new 
HttpRequestRemoteUserExtractor();
 
     private String loginServiceRealm;
     private String loginServiceProperties;
@@ -553,6 +554,20 @@ public class HttpServer {
     }
 
     /**
+     * Sets a callback implementation to defer the logic on how to use the 
right remoteUserExtractor
+     * to extract remote user.
+     *
+     * @param remoteUserExtractor User-provided remoteUserExtractor
+     * @return <code>this</code>
+     */
+
+    public Builder withRemoteUserExtractor(RemoteUserExtractor 
remoteUserExtractor) {
+      this.remoteUserExtractor = Objects.requireNonNull(remoteUserExtractor);
+      return this;
+    }
+
+
+    /**
      * Configures the server to use HTTP Basic authentication. The 
<code>properties</code> must
      * be in a form consumable by Jetty. Invoking this method overrides any 
previous call which
      * configures authentication. This authentication is supplementary to the 
JDBC-provided user
@@ -686,6 +701,7 @@ public class HttpServer {
       final String realm = b.kerberosRealm;
       final String[] additionalAllowedRealms = b.loginServiceAllowedRoles;
       final DoAsRemoteUserCallback callback = b.remoteUserCallback;
+      final RemoteUserExtractor remoteUserExtractor = b.remoteUserExtractor;
       return new AvaticaServerConfiguration() {
 
         @Override public AuthenticationType getAuthenticationType() {
@@ -709,6 +725,10 @@ public class HttpServer {
           return callback.doAsRemoteUser(remoteUserName, remoteAddress, 
action);
         }
 
+        @Override public RemoteUserExtractor getRemoteUserExtractor() {
+          return remoteUserExtractor;
+        }
+
         @Override public String[] getAllowedRoles() {
           return additionalAllowedRealms;
         }
@@ -728,6 +748,7 @@ public class HttpServer {
       final String[] allowedRoles = b.loginServiceAllowedRoles;
       final String realm = b.loginServiceRealm;
       final String properties = b.loginServiceProperties;
+      final RemoteUserExtractor remoteUserExtractor = b.remoteUserExtractor;
 
       return new AvaticaServerConfiguration() {
         @Override public AuthenticationType getAuthenticationType() {
@@ -764,6 +785,10 @@ public class HttpServer {
             Callable<T> action) throws Exception {
           return null;
         }
+
+        @Override public RemoteUserExtractor getRemoteUserExtractor() {
+          return remoteUserExtractor;
+        }
       };
     }
 

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserDisallowedException.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserDisallowedException.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserDisallowedException.java
new file mode 100644
index 0000000..227bef0
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserDisallowedException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * An exception to encapsulate that a user is disallowed to access Avatica.
+ */
+public class RemoteUserDisallowedException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public RemoteUserDisallowedException(String message) {
+    super(message);
+  }
+
+  public RemoteUserDisallowedException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
+
+// End RemoteUserDisallowedException.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractionException.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractionException.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractionException.java
new file mode 100644
index 0000000..585a758
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractionException.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * An exception thrown when encountering an error extracting a remote user 
from a request.
+ */
+public class RemoteUserExtractionException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * Creates a RemoteUserExtractionException.
+   */
+  public RemoteUserExtractionException(String message) {
+    super(message);
+  }
+
+  /**
+   * Creates a RemoteUserExtractionException with a cause.
+   */
+  public RemoteUserExtractionException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
+
+// End RemoteUserExtractionException.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractor.java
----------------------------------------------------------------------
diff --git 
a/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractor.java
 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractor.java
new file mode 100644
index 0000000..4506144
--- /dev/null
+++ 
b/server/src/main/java/org/apache/calcite/avatica/server/RemoteUserExtractor.java
@@ -0,0 +1,28 @@
+/*
+ * 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 javax.servlet.http.HttpServletRequest;
+/**
+ * Extracts remote user from request
+ */
+public interface RemoteUserExtractor {
+  String extract(HttpServletRequest request)
+      throws RemoteUserExtractionException;
+}
+
+// End RemoteUserExtractor.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/server/src/test/java/org/apache/calcite/avatica/remote/AvaticaServersForTest.java
----------------------------------------------------------------------
diff --git 
a/server/src/test/java/org/apache/calcite/avatica/remote/AvaticaServersForTest.java
 
b/server/src/test/java/org/apache/calcite/avatica/remote/AvaticaServersForTest.java
index 8e5de3a..2843a24 100644
--- 
a/server/src/test/java/org/apache/calcite/avatica/remote/AvaticaServersForTest.java
+++ 
b/server/src/test/java/org/apache/calcite/avatica/remote/AvaticaServersForTest.java
@@ -20,11 +20,13 @@ import org.apache.calcite.avatica.ConnectionSpec;
 import org.apache.calcite.avatica.Meta;
 import org.apache.calcite.avatica.jdbc.JdbcMeta;
 import org.apache.calcite.avatica.remote.Driver.Serialization;
+import org.apache.calcite.avatica.server.AvaticaHandler;
 import org.apache.calcite.avatica.server.AvaticaJsonHandler;
 import org.apache.calcite.avatica.server.AvaticaProtobufHandler;
+import org.apache.calcite.avatica.server.AvaticaServerConfiguration;
+import org.apache.calcite.avatica.server.HandlerFactory;
 import org.apache.calcite.avatica.server.HttpServer;
 import org.apache.calcite.avatica.server.Main;
-import org.apache.calcite.avatica.server.Main.HandlerFactory;
 
 import java.sql.SQLException;
 import java.util.ArrayList;
@@ -53,14 +55,14 @@ public class AvaticaServersForTest {
    */
   public void startServers() throws Exception {
     // Bind to '0' to pluck an ephemeral port instead of expecting a certain 
one to be free
-    final HttpServer jsonServer = Main.start(SERVER_ARGS, 0, new 
HandlerFactory() {
+    final HttpServer jsonServer = Main.start(SERVER_ARGS, 0, new 
Main.HandlerFactory() {
       @Override public AvaticaJsonHandler createHandler(Service service) {
         return new AvaticaJsonHandler(service);
       }
     });
     serversBySerialization.put(Serialization.JSON, jsonServer);
 
-    final HttpServer protobufServer = Main.start(SERVER_ARGS, 0, new 
HandlerFactory() {
+    final HttpServer protobufServer = Main.start(SERVER_ARGS, 0, new 
Main.HandlerFactory() {
       @Override public AvaticaProtobufHandler createHandler(Service service) {
         return new AvaticaProtobufHandler(service);
       }
@@ -69,6 +71,31 @@ public class AvaticaServersForTest {
   }
 
   /**
+   * Starts Avatica servers for each serialization type with the provided 
{@code serverConfig}.
+   */
+  public void startServers(AvaticaServerConfiguration serverConfig) {
+    final HandlerFactory factory = new HandlerFactory();
+
+    // Construct the JSON server
+    Service jsonService = new 
LocalService(FullyRemoteJdbcMetaFactory.getInstance());
+    AvaticaHandler jsonHandler = factory.getHandler(jsonService, 
Serialization.JSON, null,
+        serverConfig);
+    final HttpServer jsonServer = new 
HttpServer.Builder().withHandler(jsonHandler)
+        .withPort(0).build();
+    jsonServer.start();
+    serversBySerialization.put(Serialization.JSON, jsonServer);
+
+    // Construct the Protobuf server
+    Service protobufService = new 
LocalService(FullyRemoteJdbcMetaFactory.getInstance());
+    AvaticaHandler protobufHandler = factory.getHandler(protobufService, 
Serialization.PROTOBUF,
+        null, serverConfig);
+    final HttpServer protobufServer = new 
HttpServer.Builder().withHandler(protobufHandler)
+        .withPort(0).build();
+    protobufServer.start();
+    serversBySerialization.put(Serialization.PROTOBUF, protobufServer);
+  }
+
+  /**
    * Stops the servers currently running.
    *
    * @throws Exception If there is an error stopping a server
@@ -108,10 +135,23 @@ public class AvaticaServersForTest {
    *
    * @param port The port the Avatica server is listening on.
    * @param serialization The serialization the Avatica server is using.
-   * @return A JDBC server to the local Avatica server.
+   * @return A JDBC URL to the local Avatica server.
    */
   public String getJdbcUrl(int port, Serialization serialization) {
-    return "jdbc:avatica:remote:url=http://localhost:"; + port + 
";serialization="
+    return getJdbcUrl(port, serialization, "");
+  }
+
+  /**
+   * Computes the JDBC url for the Avatica server running on localhost, bound 
to the given port,
+   * using the given serialization, with the user-provided suffix for the HTTP 
URL to the server.
+   *
+   * @param port The port the Avatica server is listening on.
+   * @param serialization The serialization the Avatica server is using.
+   * @param urlSuffix Extra information to apend to the HTTP URL to the 
Avatica server (optional).
+   * @return A JDBC URL to the local Avatica server.
+   */
+  public String getJdbcUrl(int port, Serialization serialization, String 
urlSuffix) {
+    return "jdbc:avatica:remote:url=http://localhost:"; + port + urlSuffix + 
";serialization="
         + serialization.name();
   }
 

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/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
new file mode 100644
index 0000000..7fcde9a
--- /dev/null
+++ 
b/server/src/test/java/org/apache/calcite/avatica/server/HttpQueryStringParameterRemoteUserExtractorTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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.remote.AuthenticationType;
+import org.apache.calcite.avatica.remote.AvaticaServersForTest;
+import org.apache.calcite.avatica.remote.Driver.Serialization;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+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;
+
+/**
+ * Test class for HTTP Basic authentication with an (insecure) specification 
of the "real" user
+ * via an HTTP query string parameter.
+ *
+ * @see HttpQueryStringParameterRemoteUserExtractor
+ */
+@RunWith(Parameterized.class)
+public class HttpQueryStringParameterRemoteUserExtractorTest extends 
HttpAuthBase {
+  private static final Logger LOG =
+      
LoggerFactory.getLogger(HttpQueryStringParameterRemoteUserExtractorTest.class);
+  private static final AvaticaServersForTest SERVERS = new 
AvaticaServersForTest();
+  private static final Properties PROXY_SERVER_PROPERTIES = new Properties();
+
+  private final HttpServer server;
+  private final String missingDoAsUrl;
+  private final String disallowedDoAsUserUrl;
+  private final String allowedDoAsUserUrl;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> parameters() throws Exception {
+    SERVERS.startServers(SERVER_CONFIG);
+    return SERVERS.getJUnitParameters();
+  }
+
+  @BeforeClass
+  public static void loadProxyServerProperties() {
+    // The connecting system has one set of credentials.
+    PROXY_SERVER_PROPERTIES.put("avatica_user", "USER1");
+    PROXY_SERVER_PROPERTIES.put("avatica_password", "password1");
+  }
+
+  public HttpQueryStringParameterRemoteUserExtractorTest(Serialization 
serialization,
+      HttpServer server) throws Exception {
+    this.server = server;
+    int port = this.server.getPort();
+    // Create some JDBC urls for basic authentication with varying "doAs" 
configuration
+    missingDoAsUrl = SERVERS.getJdbcUrl(port, serialization) + 
";authentication=BASIC";
+    // USER4 is valid for HTTP basic auth, but disallowed by the server config 
(below)
+    disallowedDoAsUserUrl = SERVERS.getJdbcUrl(port, serialization, 
"?doAs=USER4")
+        + ";authentication=BASIC";
+    allowedDoAsUserUrl = SERVERS.getJdbcUrl(port,  serialization, 
"?doAs=USER2")
+        + ";authenticati0n=BASIC";
+    // Create and grant permissions to our users
+    createHsqldbUsers();
+  }
+
+  private static final AvaticaServerConfiguration SERVER_CONFIG = new 
AvaticaServerConfiguration() {
+    @Override public AuthenticationType getAuthenticationType() {
+      // HTTP Basic authentication
+      return AuthenticationType.BASIC;
+    }
+
+    @Override public String getKerberosRealm() {
+      return null;
+    }
+
+    @Override public String getKerberosPrincipal() {
+      return null;
+    }
+
+    @Override public boolean supportsImpersonation() {
+      // Impersonation is allowed
+      return true;
+    }
+
+    @Override public <T> T doAsRemoteUser(String remoteUserName, String 
remoteAddress,
+                                          Callable<T> action) throws Exception 
{
+      // We disallow the remote user "USER4", allow all others
+      if (remoteUserName.equals("USER4")) {
+        throw new RemoteUserDisallowedException("USER4 is a disallowed user!");
+      } else {
+        return action.call();
+      }
+    }
+
+    @Override public RemoteUserExtractor getRemoteUserExtractor() {
+      // We extract the "real" user via the "doAs" query string parameter
+      return new HttpQueryStringParameterRemoteUserExtractor("doAs");
+    }
+
+    @Override public String[] getAllowedRoles() {
+      return new String[] { "users" };
+    }
+
+    @Override public String getHashLoginServiceRealm() {
+      return "Avatica";
+    }
+
+    @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);
+      }
+    }
+  };
+
+  @AfterClass public static void stopServer() throws Exception {
+    if (null != SERVERS) {
+      SERVERS.stopServers();
+    }
+  }
+
+  @Test public void testUserWithDisallowedRole() throws Exception {
+    try {
+      readWriteData(missingDoAsUrl, "MISSING_DO_AS", PROXY_SERVER_PROPERTIES);
+      fail("Expected an exception");
+    } catch (RuntimeException e) {
+      LOG.info("Caught expected exception", e);
+      assertThat(e.getMessage(), containsString("Failed to execute HTTP 
Request, got HTTP/401"));
+    }
+  }
+
+  @Test public void testUserWithDisallowedDoAsRole() throws Exception {
+    try {
+      readWriteData(disallowedDoAsUserUrl, "DISALLOWED_USER", 
PROXY_SERVER_PROPERTIES);
+      fail("Expected an exception");
+    } catch (RuntimeException e) {
+      LOG.info("Caught expected exception", e);
+      assertThat(e.getMessage(), containsString("Failed to execute HTTP 
Request, got HTTP/403"));
+    }
+  }
+
+  @Test public void testAllowedDoAsUser() throws Exception {
+    // When we connect with valid credentials and provide a valid "doAs" user, 
things should work
+    readWriteData(allowedDoAsUserUrl, "ALLOWED_USER", PROXY_SERVER_PROPERTIES);
+  }
+}
+
+// End HttpQueryStringParameterRemoteUserExtractorTest.java

http://git-wip-us.apache.org/repos/asf/calcite-avatica/blob/4db1fb9c/site/_docs/security.md
----------------------------------------------------------------------
diff --git a/site/_docs/security.md b/site/_docs/security.md
index 306ace0..53233bd 100644
--- a/site/_docs/security.md
+++ b/site/_docs/security.md
@@ -230,6 +230,32 @@ public class PhoenixDoAsCallback implements 
DoAsRemoteUserCallback {
 }
 {% endhighlight %}
 
+#### Remote user extraction
+
+In some cases, it may be desirable to execute some queries on behalf of 
another user. For example,
+[Apache Knox](https://knox.apache.org) has a gateway service which can act as 
a proxy for all requests
+to the backend Avatica server. In this case, we don't want to run the queries 
as the Knox user, instead
+the real user communicating with Knox.
+
+There are presently two options to extract the "real" user from HTTP requests:
+
+* The authenticated user from the HTTP request, 
`org.apache.calcite.avatica.server.HttpRequestRemoteUserExtractor` (default)
+* The value of a parameter in the HTTP query string, 
`org.apache.calcite.avatica.server.HttpQueryStringParameterRemoteUserExtractor` 
(e.g "doAs")
+
+Implementations of Avatica can configure this using the 
`AvaticaServerConfiguration` and providing
+an implementation of `RemoteUserExtractor`. There are two implementations 
provided as listed above.
+
+{% highlight java %}
+config = new AvaticaServerConfiguration() {
+  /* ... */
+  @Override public RemoteUserExtractor getRemoteUserExtractor() {
+    // We extract the "real" user via the "doAs" query string parameter
+    return new HttpQueryStringParameterRemoteUserExtractor("doAs");
+  }
+  /* ... */
+};
+{% endhighlight %}
+
 ## Client implementation
 
 Many HTTP client libraries, such as [Apache Commons 
HttpComponents](https://hc.apache.org/), already have

Reply via email to