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
