DRILL-5726: Support Impersonation without authentication for REST API DRILL-5726: Changes after code review.
close apache/drill#910 Project: http://git-wip-us.apache.org/repos/asf/drill/repo Commit: http://git-wip-us.apache.org/repos/asf/drill/commit/789b83d7 Tree: http://git-wip-us.apache.org/repos/asf/drill/tree/789b83d7 Diff: http://git-wip-us.apache.org/repos/asf/drill/diff/789b83d7 Branch: refs/heads/master Commit: 789b83d773b9022638c3e9841e9f13a7033cb60b Parents: 2c470de Author: Arina Ielchiieva <arina.yelchiy...@gmail.com> Authored: Wed Aug 23 16:17:11 2017 +0300 Committer: Aman Sinha <asi...@maprtech.com> Committed: Sat Sep 2 23:00:07 2017 -0700 ---------------------------------------------------------------------- .../drill/exec/server/rest/DrillRestServer.java | 28 +++++++-- .../drill/exec/server/rest/QueryResources.java | 7 ++- .../drill/exec/server/rest/UserNameFilter.java | 61 ++++++++++++++++++++ .../drill/exec/server/rest/WebServer.java | 19 +++++- .../src/main/resources/rest/query/query.ftl | 48 ++++++++++++++- 5 files changed, 153 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/drill/blob/789b83d7/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java ---------------------------------------------------------------------- diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java index e88d1b0..bd01fea 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/DrillRestServer.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper; import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper; import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import com.google.common.base.Strings; import org.apache.drill.common.config.DrillConfig; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.memory.BufferAllocator; @@ -188,7 +189,6 @@ public class DrillRestServer extends ResourceConfig { @Override public WebUserConnection provide() { - final HttpSession session = request.getSession(); final DrillbitContext drillbitContext = workManager.getContext(); final DrillConfig config = drillbitContext.getConfig(); @@ -198,9 +198,9 @@ public class DrillRestServer extends ResourceConfig { config.getLong(ExecConstants.HTTP_SESSION_MEMORY_RESERVATION), config.getLong(ExecConstants.HTTP_SESSION_MEMORY_MAXIMUM)); - final Principal sessionUserPrincipal = new AnonDrillUserPrincipal(); + final Principal sessionUserPrincipal = createSessionUserPrincipal(config, request); - // Create new UserSession for each request from Anonymous user + // Create new UserSession for each request from non-authenticated user final UserSession drillUserSession = UserSession.Builder.newBuilder() .withCredentials(UserBitShared.UserCredentials.newBuilder() .setUserName(sessionUserPrincipal.getName()) @@ -230,6 +230,26 @@ public class DrillRestServer extends ResourceConfig { public void dispose(WebUserConnection instance) { } + + /** + * Creates session user principal. If impersonation is enabled without authentication and User-Name header is present and valid, + * will create session user principal with provided user name, otherwise anonymous user name will be used. + * In both cases session user principal will have admin rights. + * + * @param config drill config + * @param request client request + * @return session user principal + */ + private Principal createSessionUserPrincipal(DrillConfig config, HttpServletRequest request) { + if (WebServer.isImpersonationOnlyEnabled(config)) { + final String userName = request.getHeader("User-Name"); + if (!Strings.isNullOrEmpty(userName)) { + return new DrillUserPrincipal(userName, true); + } + } + return new AnonDrillUserPrincipal(); + } + } // Provider which injects DrillUserPrincipal directly instead of getting it from SecurityContext and typecasting http://git-wip-us.apache.org/repos/asf/drill/blob/789b83d7/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java ---------------------------------------------------------------------- diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java index 99e26ff..c46c4b5 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/QueryResources.java @@ -54,7 +54,12 @@ public class QueryResources { @Path("/query") @Produces(MediaType.TEXT_HTML) public Viewable getQuery() { - return ViewableWithPermissions.create(authEnabled.get(), "/rest/query/query.ftl", sc); + return ViewableWithPermissions.create( + authEnabled.get(), + "/rest/query/query.ftl", + sc, + // if impersonation is enabled without authentication, will provide mechanism to add user name to request header from Web UI + WebServer.isImpersonationOnlyEnabled(work.getContext().getConfig())); } @POST http://git-wip-us.apache.org/repos/asf/drill/blob/789b83d7/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/UserNameFilter.java ---------------------------------------------------------------------- diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/UserNameFilter.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/UserNameFilter.java new file mode 100644 index 0000000..b8c898c --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/UserNameFilter.java @@ -0,0 +1,61 @@ +/* + * 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 + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * 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.drill.exec.server.rest; + +import com.google.common.base.Strings; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.HttpMethod; +import java.io.IOException; + +/** + * Responsible for filtering out POST requests that do not contain valid <b>User-Name</b> header. + */ +public class UserNameFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) resp; + + if (HttpMethod.POST.equalsIgnoreCase(request.getMethod()) && Strings.isNullOrEmpty(request.getHeader("User-Name"))) { + response.reset(); + response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, "User-Name header is not set"); + return; + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/drill/blob/789b83d7/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java ---------------------------------------------------------------------- diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java index 1706b71..3fc95cd 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/WebServer.java @@ -1,4 +1,4 @@ -/** +/* * 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 @@ -20,7 +20,6 @@ package org.apache.drill.exec.server.rest; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.servlets.MetricsServlet; import com.codahale.metrics.servlets.ThreadDumpServlet; -import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -126,6 +125,16 @@ public class WebServer implements AutoCloseable { private static final String DRILL_ICON_RESOURCE_RELATIVE_PATH = "img/drill.ico"; /** + * Checks if only impersonation is enabled. + * + * @param config Drill configuration + * @return true if impersonation without authentication is enabled, false otherwise + */ + public static boolean isImpersonationOnlyEnabled(DrillConfig config) { + return !config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED) && config.getBoolean(ExecConstants.IMPERSONATION_ENABLED); + } + + /** * Start the web server including setup. * @throws Exception */ @@ -185,6 +194,12 @@ public class WebServer implements AutoCloseable { servletContextHandler.setSessionHandler(createSessionHandler(servletContextHandler.getSecurityHandler())); } + if (isImpersonationOnlyEnabled(workManager.getContext().getConfig())) { + for (String path : new String[]{"/query", "/query.json"}) { + servletContextHandler.addFilter(UserNameFilter.class, path, EnumSet.of(DispatcherType.REQUEST)); + } + } + if (config.getBoolean(ExecConstants.HTTP_CORS_ENABLED)) { FilterHolder holder = new FilterHolder(CrossOriginFilter.class); holder.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, http://git-wip-us.apache.org/repos/asf/drill/blob/789b83d7/exec/java-exec/src/main/resources/rest/query/query.ftl ---------------------------------------------------------------------- diff --git a/exec/java-exec/src/main/resources/rest/query/query.ftl b/exec/java-exec/src/main/resources/rest/query/query.ftl index 5033aca..f9765eb 100644 --- a/exec/java-exec/src/main/resources/rest/query/query.ftl +++ b/exec/java-exec/src/main/resources/rest/query/query.ftl @@ -11,6 +11,9 @@ <#include "*/generic.ftl"> <#macro page_head> + <#if model?? && model> + <script src="/static/js/jquery.form.js"></script> + </#if> </#macro> <#macro page_body> @@ -21,8 +24,16 @@ <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> Sample SQL query: <strong>SELECT * FROM cp.`employee.json` LIMIT 20</strong> </div> - <form role="form" action="/query" method="POST"> - <div class="form-group"> + + <#if model?? && model> + <div class="form-group"> + <label for="userName">User Name</label> + <input type="text" size="30" name="userName" id="userName" placeholder="User Name"> + </div> + </#if> + + <form role="form" id="queryForm" action="/query" method="POST"> + <div class="form-group"> <label for="queryType">Query Type</label> <div class="radio"> <label> @@ -47,8 +58,39 @@ <label for="query">Query</label> <textarea class="form-control" id="query" rows="5" name="query" style="font-family: Courier;"></textarea> </div> - <button type="submit" class="btn btn-default">Submit</button> + + <button class="btn btn-default" type=<#if model?? && model>"button" onclick="doSubmit()"<#else>"submit"</#if>> + Submit + </button> </form> + + <#if model?? && model> + <script> + function doSubmit() { + var userName = document.getElementById("userName").value; + if (!userName.trim()) { + alert("Please fill in User Name field"); + return; + } + $.ajax({ + type: "POST", + beforeSend: function (request) { + request.setRequestHeader("User-Name", userName); + }, + url: "/query", + data: $("#queryForm").serializeArray(), + success: function (response) { + var newDoc = document.open("text/html", "replace"); + newDoc.write(response); + newDoc.close(); + }, + error: function (request, textStatus, errorThrown) { + alert(errorThrown); + } + }); + } + </script> + </#if> </#macro> <@page_html/>