From c770a2e7b0e75c4f18b954d8ada36469f00c8355 Mon Sep 17 00:00:00 2001 From: mapr Date: Mon, 11 Sep 2017 16:56:22 -0700 Subject: [PATCH 1/3] Added comments --- .../org/apache/drill/exec/ExecConstants.java | 4 + .../server/rest/LogInLogOutResources.java | 38 +++- .../server/rest/ViewableWithPermissions.java | 30 ++-- .../drill/exec/server/rest/WebServer.java | 12 +- .../auth/DrillConstraintSecurityHandler.java | 38 ++++ .../server/rest/auth/DrillErrorHandler.java | 52 ++++++ .../rest/auth/DrillSecurityHandler.java | 164 ++++++++++++++++++ .../rest/auth/DrillSpnegoAuthenticator.java | 141 +++++++++++++++ .../rest/auth/DrillSpnegoLoginService.java | 154 ++++++++++++++++ .../exec/server/rest/auth/SpnegoUtil.java | 86 +++++++++ .../src/main/resources/rest/generic.ftl | 2 +- .../src/main/resources/rest/mainlogin.ftl | 26 +++ 12 files changed, 727 insertions(+), 20 deletions(-) create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java create mode 100644 exec/java-exec/src/main/resources/rest/mainlogin.ftl diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java index 95ee00e715c..dd1e3f7edac 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java @@ -122,6 +122,10 @@ public interface ExecConstants { String HTTP_SESSION_MEMORY_RESERVATION = "drill.exec.http.session.memory.reservation"; String HTTP_SESSION_MEMORY_MAXIMUM = "drill.exec.http.session.memory.maximum"; String HTTP_SESSION_MAX_IDLE_SECS = "drill.exec.http.session_max_idle_secs"; + String HTTP_AUTHENTICATION_MECHANISMS = "drill.exec.http.auth.mechanisms"; + String HTTP_SPNEGO_PRINCIPAL_= "drill.exec.http.spnego.auth.principal"; + String HTTP_SPNEGO_KEYTAB_= "drill.exec.http.spnego.auth.keytab"; + String HTTP_SPNEGO_REALM_= "drill.exec.http.spnego.auth.realm"; String HTTP_KEYSTORE_PATH = "drill.exec.ssl.keyStorePath"; String HTTP_KEYSTORE_PASSWORD = "drill.exec.ssl.keyStorePassword"; String HTTP_TRUSTSTORE_PATH = "drill.exec.ssl.trustStorePath"; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java index 20cd6da1d8e..f30e62fe4bd 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java @@ -44,14 +44,14 @@ @PermitAll public class LogInLogOutResources { public static final String REDIRECT_QUERY_PARM = "redirect"; - public static final String LOGIN_RESOURCE = "login"; + public static final String LOGIN_RESOURCE = "mainlogin"; @GET @Path("/login") @Produces(MediaType.TEXT_HTML) public Viewable getLoginPage(@Context HttpServletRequest request, @Context HttpServletResponse response, - @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) - throws Exception { + @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) + throws Exception { if (AuthDynamicFeature.isUserLoggedIn(sc)) { // if the user is already login, forward the request to homepage. request.getRequestDispatcher("/").forward(request, response); @@ -69,6 +69,20 @@ public Viewable getLoginPage(@Context HttpServletRequest request, @Context HttpS return ViewableWithPermissions.createLoginPage(null); } + @GET + @Path("/sn") + @Produces(MediaType.TEXT_HTML) + public Viewable getspnegologin(@Context HttpServletRequest request, @Context HttpServletResponse response, + @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) + throws Exception { + if (AuthDynamicFeature.isUserLoggedIn(sc)) { + request.getRequestDispatcher("/").forward(request, response); + return null; + } + + return ViewableWithPermissions.createmainLoginPage("Invalid SPNEGO credentials."); + } + // Request type is POST because POST request which contains the login credentials are invalid and the request is // dispatched here directly. @POST @@ -88,4 +102,22 @@ public void logout(@Context HttpServletRequest req, @Context HttpServletResponse req.getRequestDispatcher("/").forward(req, resp); } + @GET + @Path("/mainlogin") + @Produces(MediaType.TEXT_HTML) + public Viewable getMainLoginPage(@Context HttpServletRequest request, @Context HttpServletResponse response, + @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) + throws Exception { + //req.getRequestDispatcher("/").forward(req, resp); + if (!StringUtils.isEmpty(redirect)) { + // If the URL has redirect in it, set the redirect URI in session, so that after the login is successful, request + // is forwarded to the redirect page. + final HttpSession session = request.getSession(true); + final URI destURI = UriBuilder.fromUri(URLDecoder.decode(redirect, "UTF-8")).build(); + session.setAttribute(FormAuthenticator.__J_URI, destURI.toString()); + } + return ViewableWithPermissions.createmainLoginPage(null); + + } + } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java index 73019aa11a3..fb56cfd0ab0 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java @@ -48,7 +48,7 @@ public static Viewable create(final boolean authEnabled, final String templateNa * @return */ public static Viewable create(final boolean authEnabled, final String templateName, final SecurityContext sc, - final Object model) { + final Object model) { return new ViewableWithPermissions(authEnabled, templateName, sc, true, model); } @@ -61,29 +61,33 @@ public static Viewable createLoginPage(final String errorMsg) { return new ViewableWithPermissions(true, "/rest/login.ftl", null, false, errorMsg); } + public static Viewable createmainLoginPage(final String errorMsg) { + return new ViewableWithPermissions(true, "/rest/mainlogin.ftl", null, false, errorMsg); + } + private ViewableWithPermissions(final boolean authEnabled, final String templateName, final SecurityContext sc, - final boolean showControls, final Object model) throws IllegalArgumentException { + final boolean showControls, final Object model) throws IllegalArgumentException { super(templateName, createModel(authEnabled, sc, showControls, model)); } private static Map createModel(final boolean authEnabled, final SecurityContext sc, - final boolean showControls, final Object pageModel) { + final boolean showControls, final Object pageModel) { final boolean isAdmin = !authEnabled /* when auth is disabled every user is an admin user */ - || (showControls && sc.isUserInRole(DrillUserPrincipal.ADMIN_ROLE)); + || (showControls && sc.isUserInRole(DrillUserPrincipal.ADMIN_ROLE)); final boolean isUserLoggedIn = AuthDynamicFeature.isUserLoggedIn(sc); final ImmutableMap.Builder mapBuilder = ImmutableMap.builder() - .put("showStorage", isAdmin) - .put("showOptions", isAdmin) - .put("showThreads", isAdmin) - .put("showLogs", isAdmin) - .put("showLogin", authEnabled && showControls && !isUserLoggedIn) - .put("showLogout", authEnabled && showControls && isUserLoggedIn) - .put("loggedInUserName", authEnabled && showControls && - isUserLoggedIn ? sc.getUserPrincipal().getName() : DrillUserPrincipal.ANONYMOUS_USER) - .put("showControls", showControls); + .put("showStorage", isAdmin) + .put("showOptions", isAdmin) + .put("showThreads", isAdmin) + .put("showLogs", isAdmin) + .put("showLogin", authEnabled && showControls && !isUserLoggedIn) + .put("showLogout", authEnabled && showControls && isUserLoggedIn) + .put("loggedInUserName", authEnabled && showControls && + isUserLoggedIn ? sc.getUserPrincipal().getName() : DrillUserPrincipal.ANONYMOUS_USER) + .put("showControls", showControls); if (pageModel != null) { mapBuilder.put("model", pageModel); 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 3fc95cd8338..afe4ba843ce 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 @@ -21,6 +21,7 @@ import com.codahale.metrics.servlets.MetricsServlet; import com.codahale.metrics.servlets.ThreadDumpServlet; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.drill.common.exceptions.DrillException; @@ -31,6 +32,7 @@ import org.apache.drill.exec.rpc.security.plain.PlainFactory; import org.apache.drill.exec.server.BootStrapContext; import org.apache.drill.exec.server.rest.auth.DrillRestLoginService; +import org.apache.drill.exec.server.rest.auth.DrillSecurityHandler; import org.apache.drill.exec.work.WorkManager; import org.bouncycastle.asn1.x500.X500NameBuilder; import org.bouncycastle.asn1.x500.style.BCStyle; @@ -80,6 +82,7 @@ import java.util.Collections; import java.util.Date; import java.util.EnumSet; +import java.util.List; import java.util.Set; import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_ROLE; @@ -101,6 +104,8 @@ public class WebServer implements AutoCloseable { private final BootStrapContext context; + private final String SpnegoAuth = "SPNEGO"; + /** * Create Jetty based web server. * @@ -142,13 +147,12 @@ public void start() throws Exception { if (embeddedJetty == null) { return; } - final boolean authEnabled = config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED); + boolean authEnabled = config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED); if (authEnabled && !context.getAuthProvider().containsFactory(PlainFactory.SIMPLE_NAME)) { logger.warn("Not starting web server. Currently Drill supports web authentication only through " + "username/password. But PLAIN mechanism is not configured."); return; } - final ServerConnector serverConnector; if (config.getBoolean(ExecConstants.HTTP_ENABLE_SSL)) { try { @@ -190,7 +194,9 @@ public void start() throws Exception { servletContextHandler.addServlet(staticHolder, "/static/*"); if (authEnabled) { - servletContextHandler.setSecurityHandler(createSecurityHandler()); + //DrillSecurityHandler is used to support SPNEGO and FORM authentication together + DrillSecurityHandler securityHandler = new DrillSecurityHandler(config, workManager); + servletContextHandler.setSecurityHandler(securityHandler); servletContextHandler.setSessionHandler(createSessionHandler(servletContextHandler.getSecurityHandler())); } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java new file mode 100644 index 00000000000..c0cff32d4ba --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java @@ -0,0 +1,38 @@ +/** + * 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.drill.exec.server.rest.auth; + +import org.eclipse.jetty.security.ConstraintSecurityHandler; + + +public class DrillConstraintSecurityHandler extends ConstraintSecurityHandler { + + public DrillConstraintSecurityHandler() { + + } + + @Override + public void doStart() throws Exception { + super.doStart(); + } + + @Override + public void doStop() throws Exception { + super.doStop(); + } +} diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java new file mode 100644 index 00000000000..da9e42aa141 --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java @@ -0,0 +1,52 @@ +/* + * 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.drill.exec.server.rest.auth; + +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.ErrorHandler; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.io.Writer; + + +public class DrillErrorHandler extends ErrorHandler { + + + + @Override + protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, int code, String message, String uri) throws IOException { + + super.writeErrorPageMessage(request,writer,code,message,uri); + //writer.write("

Spnego login failed

"); + String path = request.getContextPath(); + String ref = path+"/login"; + writer.write(" root "); + } + } +} diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java new file mode 100644 index 00000000000..4bcd71756b2 --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java @@ -0,0 +1,164 @@ +/* + * 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.drill.exec.server.rest.auth; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import org.apache.drill.common.config.DrillConfig; +import org.apache.drill.common.exceptions.DrillException; +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.exec.exception.DrillbitStartupException; +import org.apache.drill.exec.work.WorkManager; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.security.ConstraintMapping; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.DefaultIdentityService; +import org.eclipse.jetty.security.IdentityService; +import org.eclipse.jetty.security.SpnegoLoginService; +import org.eclipse.jetty.security.authentication.FormAuthenticator; +import org.eclipse.jetty.security.authentication.SessionAuthentication; +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.ADMIN_ROLE; +import static org.apache.drill.exec.server.rest.auth.DrillUserPrincipal.AUTHENTICATED_ROLE; + + +public class DrillSecurityHandler extends ConstraintSecurityHandler { + + private DrillConfig DrillConfig; + private final WorkManager workManager; + private DrillConstraintSecurityHandler basicSecurityHandler; + private DrillConstraintSecurityHandler spnegoSecurityHandler; + private final String HTTP_FORM ="FORM"; + private final String HTTP_SPNEGO ="SPNEGO"; + private List configuredMechanisms = Lists.newArrayList(); + private Set knownRoles = ImmutableSet.of(AUTHENTICATED_ROLE, ADMIN_ROLE); + private SpnegoUtil spnUtil; + + public DrillSecurityHandler(DrillConfig config,WorkManager work) throws DrillbitStartupException,IOException { + DrillConfig = config; + spnUtil = new SpnegoUtil(config); + this.workManager = work; + try { + if (config.hasPath(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS)) { + configuredMechanisms = config.getStringList(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS); + if (configuredMechanisms.contains(HTTP_FORM)) { + initializeFormAuthentication(); + + } + if (configuredMechanisms.contains(HTTP_SPNEGO)) { + initializeSpnegoAuthentication(); + + } + } + } + catch(DrillException e){ + throw new DrillbitStartupException(e.getMessage(), e); + } + //Backward compatability for Form authentication + if(!config.hasPath(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS)){ + initializeFormAuthentication(); + } + } + + public void initializeFormAuthentication(){ + basicSecurityHandler = new DrillConstraintSecurityHandler(); + basicSecurityHandler.setConstraintMappings(Collections.emptyList(), knownRoles); + basicSecurityHandler.setAuthenticator(new FormAuthenticator("/login", "/login", true)); + basicSecurityHandler.setLoginService(new DrillRestLoginService(workManager.getContext())); + + + } + + public void initializeSpnegoAuthentication() throws DrillException { + spnegoSecurityHandler = new DrillConstraintSecurityHandler(); + spnegoSecurityHandler.setAuthenticator(new DrillSpnegoAuthenticator()); + final SpnegoLoginService loginService = new DrillSpnegoLoginService(spnUtil.getSpnegoRealm(),spnUtil.getSpnegoPrincipal(),workManager.getContext(),spnUtil.getUgi()); + final IdentityService identityService = new DefaultIdentityService(); + loginService.setIdentityService(identityService); + spnegoSecurityHandler.setLoginService(loginService); + spnegoSecurityHandler.setConstraintMappings(Collections.emptyList(),knownRoles); + + } + + @Override + public void doStart() throws Exception { + super.doStart(); + if(!configuredMechanisms.isEmpty() && configuredMechanisms.contains(HTTP_SPNEGO)) { + spnegoSecurityHandler.doStart(); + } + if(configuredMechanisms.isEmpty() || configuredMechanisms.contains(HTTP_FORM) ) { + basicSecurityHandler.doStart(); + } + + } + + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest)baseRequest; + String header = req.getHeader(HttpHeader.AUTHORIZATION.asString()); + HttpSession session = req.getSession(true); + SessionAuthentication authentication = (SessionAuthentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); + String uri = req.getRequestURI(); + //Before authentication, all requests go through the Formauthenticator except for Spnegologin request + //If this authentication is null, user hasn't logged in yet + if(authentication == null){ + if( uri.equals("/sn") ){ + spnegoSecurityHandler.handle(target, baseRequest, request, response); + } + else { + basicSecurityHandler.handle(target, baseRequest, request, response); + } + + } + //If user has logged in, use the corresponding handler to handle the request + else{ + if(authentication.getAuthMethod() == "FORM"){ + basicSecurityHandler.handle(target, baseRequest, request, response); + } + else if(authentication.getAuthMethod() == "SPNEGO"){ + spnegoSecurityHandler.handle(target, baseRequest, request, response); + } + } + } + + @Override + public void setHandler(Handler handler) { + super.setHandler(handler); + if(!configuredMechanisms.isEmpty() && configuredMechanisms.contains(HTTP_SPNEGO)) { + spnegoSecurityHandler.setHandler(handler); + } + if(configuredMechanisms.isEmpty() || configuredMechanisms.contains(HTTP_SPNEGO)) { + basicSecurityHandler.setHandler(handler); + } + } + } \ No newline at end of file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java new file mode 100644 index 00000000000..3ecfa92f438 --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java @@ -0,0 +1,141 @@ +/* + * 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.drill.exec.server.rest.auth; + + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.security.ServerAuthException; +import org.eclipse.jetty.security.SpnegoLoginService; +import org.eclipse.jetty.security.UserAuthentication; +import org.eclipse.jetty.security.authentication.DeferredAuthentication; +import org.eclipse.jetty.security.authentication.FormAuthenticator; +import org.eclipse.jetty.security.authentication.SessionAuthentication; +import org.eclipse.jetty.security.authentication.SpnegoAuthenticator; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.server.Request; + +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +public class DrillSpnegoAuthenticator extends SpnegoAuthenticator { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillSpnegoAuthenticator.class); + + public DrillSpnegoAuthenticator() { + + } + + + @Override + public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException { + + HttpServletRequest req = (HttpServletRequest) request; + HttpServletResponse res = (HttpServletResponse) response; + HttpSession session = req.getSession(true); + Authentication authentication = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); + String uri = req.getRequestURI(); + if( uri.equals("/sn")){ + mandatory = true; + } + if (authentication != null && uri.equals("/logout")) { + session.removeAttribute("org.eclipse.jetty.security.UserIdentity"); + Authentication auth = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); + return auth; + } + else if (authentication != null) { + return authentication; + } + else { + String header = req.getHeader(HttpHeader.AUTHORIZATION.asString()); + if (!mandatory) { + return new DeferredAuthentication(this); + } else if (header == null) { + try { + if (DeferredAuthentication.isDeferred(res)) { + return Authentication.UNAUTHENTICATED; + } else { + res.setHeader(HttpHeader.WWW_AUTHENTICATE.asString(), HttpHeader.NEGOTIATE.asString()); + res.sendError(401); + return Authentication.SEND_CONTINUE; + } + } catch (IOException var9) { + throw new ServerAuthException(var9); + } + } else { + if (header != null && header.startsWith(HttpHeader.NEGOTIATE.asString())) { + String spnegoToken = header.substring(10); + UserIdentity user = this.login((String) null, spnegoToken, request); + //redirect the request to the desired page after successful login + if (user != null) { + String nuri; + synchronized(session) { + nuri = (String)session.getAttribute("org.eclipse.jetty.security.form_URI"); + if(nuri == null || nuri.length() == 0) { + nuri = req.getContextPath(); + if(nuri.length() == 0) { + nuri = "/"; + } + } + } + response.setContentLength(0); + Response base_response = HttpChannel.getCurrentHttpChannel().getResponse(); + Request base_request = HttpChannel.getCurrentHttpChannel().getRequest(); + int redirectCode = base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()?302:303; + try { + base_response.sendRedirect(redirectCode, res.encodeRedirectURL(nuri)); + } catch (IOException e) { + e.printStackTrace(); + } + return new UserAuthentication(this.getAuthMethod(), user); + } + + } + + return Authentication.UNAUTHENTICATED; + } + } + + } + + + public UserIdentity login(String username, Object password, ServletRequest request) { + UserIdentity user = super.login(username, password, request); + if (user != null) { + HttpSession session = ((HttpServletRequest) request).getSession(true); + Authentication cached = new SessionAuthentication(this.getAuthMethod(), user, password); + session.setAttribute("org.eclipse.jetty.security.UserIdentity", cached); + } + + return user; + } + + +} diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java new file mode 100644 index 00000000000..0e6e8108412 --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java @@ -0,0 +1,154 @@ +/* + * 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.drill.exec.server.rest.auth; + + +import org.apache.drill.common.config.DrillConfig; +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.exec.expr.fn.impl.ContextFunctions; +import org.apache.drill.exec.server.DrillbitContext; +import org.apache.drill.exec.server.options.SystemOptionManager; +import org.apache.drill.exec.util.ImpersonationUtil; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.security.UserGroupInformation; +import org.eclipse.jetty.security.SpnegoLoginService; +import org.eclipse.jetty.security.SpnegoUserPrincipal; +import org.eclipse.jetty.server.UserIdentity; +import org.eclipse.jetty.util.B64Code; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import java.lang.reflect.Field; +import java.security.PrivilegedExceptionAction; +import java.util.Objects; + +public class DrillSpnegoLoginService extends SpnegoLoginService { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillSpnegoLoginService.class); + + private static final String TARGET_NAME_FIELD_NAME = "_targetName"; + private final String serverPrincipal; + private final DrillbitContext drillContext; + private UserIdentity identity; + private final UserGroupInformation ugi; + + + + public DrillSpnegoLoginService(String realm, String serverPrincipal, DrillbitContext drillbitContext, UserGroupInformation ugSpnego) { + super(realm); + this.serverPrincipal = Objects.requireNonNull(serverPrincipal); + drillContext = drillbitContext; + ugi = ugSpnego; + } + + @Override + protected void doStart() throws Exception { + // Override the parent implementation, setting _targetName to be the serverPrincipal + // without the need for a one-line file to do the same thing. + // + // AbstractLifeCycle's doStart() method does nothing, so we aren't missing any extra logic. + final Field targetNameField = SpnegoLoginService.class.getDeclaredField(TARGET_NAME_FIELD_NAME); + targetNameField.setAccessible(true); + targetNameField.set(this, serverPrincipal); + } + + @Override + public UserIdentity login(final String username, final Object credentials) { + try { + identity = ugi.doAs(new PrivilegedExceptionAction() { + @Override + public UserIdentity run() { + return spnegologin(username, credentials); + } + }); + } catch (Exception e) { + logger.error("Failed to login using SPNEGO"); + } + return identity; + + } + + public UserIdentity spnegologin(String username, Object credentials) { + + String encodedAuthToken = (String) credentials; + byte[] authToken = B64Code.decode(encodedAuthToken); + + GSSManager manager = GSSManager.getInstance(); + try { + + Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); + Oid krb5Oid = new Oid("1.2.840.113554.1.2.2"); + GSSName gssName = manager.createName(serverPrincipal, null); + GSSCredential serverCreds = manager.createCredential(gssName, + GSSCredential.INDEFINITE_LIFETIME, new Oid[] {krb5Oid, spnegoOid}, + GSSCredential.ACCEPT_ONLY); + GSSContext gContext = manager.createContext(serverCreds); + if (gContext == null) { + logger.debug("SpnegoUserRealm: failed to establish GSSContext"); + } else { + while (!gContext.isEstablished()) { + authToken = gContext.acceptSecContext(authToken, 0, authToken.length); + } + if (gContext.isEstablished()) { + String clientName = gContext.getSrcName().toString(); + String role = clientName.substring(0, clientName.indexOf(64)); + // LOG.debug("SpnegoUserRealm: established a security context", new Object[0]); + //LOG.debug("Client Principal is: " + gContext.getSrcName(), new Object[0]); + // LOG.debug("Server Principal is: " + gContext.getTargName(), new Object[0]); + // LOG.debug("Client Default Role: " + role, new Object[0]); + final SystemOptionManager sysOptions = drillContext.getOptionManager(); + + final boolean isAdmin = ImpersonationUtil.hasAdminPrivileges(role, + sysOptions.getOption(ExecConstants.ADMIN_USERS_KEY).string_val, + sysOptions.getOption(ExecConstants.ADMIN_USER_GROUPS_KEY).string_val); + DrillUserPrincipal user = new DrillUserPrincipal(clientName, isAdmin); + Subject subject = new Subject(); + subject.getPrincipals().add(user); + if (isAdmin) { + return this._identityService.newUserIdentity(subject, user, DrillUserPrincipal.ADMIN_USER_ROLES); + } else { + return this._identityService.newUserIdentity(subject, user, DrillUserPrincipal.NON_ADMIN_USER_ROLES); + } + } + } + } catch (GSSException gsse) { + logger.warn("Caught GSSException trying to authenticate the client", gsse); + } + + return null; + + } + + @Override + public boolean validate(UserIdentity user) { + return true; + } + + + +} + diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java new file mode 100644 index 00000000000..a7103c30b30 --- /dev/null +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java @@ -0,0 +1,86 @@ +/* + * 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.drill.exec.server.rest.auth; + + +import org.apache.drill.common.config.DrillConfig; +import org.apache.drill.common.exceptions.DrillException; +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.exec.exception.DrillbitStartupException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.CommonConfigurationKeys; +import org.apache.hadoop.security.UserGroupInformation; + +import java.io.IOException; + +public class SpnegoUtil { + + private final DrillConfig config; + private UserGroupInformation ugi; + private String realm; + private String principal; + private String keytab; + public SpnegoUtil(DrillConfig configuration){ + this.config = configuration; + } + + public String getSpnegoRealm() throws DrillException { + realm = config.getString(ExecConstants.HTTP_SPNEGO_REALM_).trim(); + if(realm.isEmpty()){ + throw new DrillException(" drill.exec.http.spnego.auth.realm in the configuration file can't be empty"); + } + return realm; + } + + + public String getSpnegoPrincipal() throws DrillException { + principal = config.getString(ExecConstants.HTTP_SPNEGO_PRINCIPAL_).trim(); + if(principal.isEmpty()){ + throw new DrillException(" drill.exec.http.spnego.auth.principal in the configuration file can't be empty"); + } + return principal; + } + + public String getSpnegoKeytab() throws DrillException { + keytab = config.getString(ExecConstants.HTTP_SPNEGO_KEYTAB_).trim(); + if(keytab.isEmpty()){ + throw new DrillException(" drill.exec.http.spnego.auth.keytab in the configuration file can't be empty"); + } + return keytab; + } + + public UserGroupInformation getUgi() throws DrillException { + if (!UserGroupInformation.isSecurityEnabled()) { + final Configuration Config = new Configuration(); + Config.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, + UserGroupInformation.AuthenticationMethod.KERBEROS.toString()); + UserGroupInformation.setConfiguration(Config); + } + try{ + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal,this.getSpnegoKeytab()); + + } + catch (Exception e){ + throw new DrillException("Login failed for"+principal+"with given keytab"); + } + + return ugi; + } +} diff --git a/exec/java-exec/src/main/resources/rest/generic.ftl b/exec/java-exec/src/main/resources/rest/generic.ftl index 9025adb1f80..a7cee1d7034 100644 --- a/exec/java-exec/src/main/resources/rest/generic.ftl +++ b/exec/java-exec/src/main/resources/rest/generic.ftl @@ -77,7 +77,7 @@

  • Documentation <#if showLogin == true > -
  • Log In +
  • Log In <#if showLogout == true >
  • Log Out (${loggedInUserName}) diff --git a/exec/java-exec/src/main/resources/rest/mainlogin.ftl b/exec/java-exec/src/main/resources/rest/mainlogin.ftl new file mode 100644 index 00000000000..b406d450fff --- /dev/null +++ b/exec/java-exec/src/main/resources/rest/mainlogin.ftl @@ -0,0 +1,26 @@ +<#-- 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. --> +<#include "*/generic.ftl"> +<#macro page_head> + + +<#macro page_body> + + +<@page_html/> \ No newline at end of file From 6015e98f08c1961bbbc9911946522f9a076e87e0 Mon Sep 17 00:00:00 2001 From: mapr Date: Wed, 13 Sep 2017 12:48:28 -0700 Subject: [PATCH 2/3] Added Test Cases --- .../rest/auth/DrillSecurityHandler.java | 20 +-- .../rest/auth/DrillSpnegoAuthenticator.java | 16 +- .../exec/server/rest/auth/SpnegoUtil.java | 26 +-- .../src/main/resources/rest/mainlogin.ftl | 16 +- .../drill/exec/server/SpnegoTestUtils.java | 154 ++++++++++++++++++ .../exec/server/TestSpnegoAuthentication.java | 127 +++++++++++++++ 6 files changed, 320 insertions(+), 39 deletions(-) create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/server/SpnegoTestUtils.java create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java index 4bcd71756b2..5ce36a84b17 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java @@ -42,7 +42,6 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; @@ -53,7 +52,6 @@ public class DrillSecurityHandler extends ConstraintSecurityHandler { - private DrillConfig DrillConfig; private final WorkManager workManager; private DrillConstraintSecurityHandler basicSecurityHandler; private DrillConstraintSecurityHandler spnegoSecurityHandler; @@ -64,7 +62,6 @@ public class DrillSecurityHandler extends ConstraintSecurityHandler { private SpnegoUtil spnUtil; public DrillSecurityHandler(DrillConfig config,WorkManager work) throws DrillbitStartupException,IOException { - DrillConfig = config; spnUtil = new SpnegoUtil(config); this.workManager = work; try { @@ -94,8 +91,6 @@ public void initializeFormAuthentication(){ basicSecurityHandler.setConstraintMappings(Collections.emptyList(), knownRoles); basicSecurityHandler.setAuthenticator(new FormAuthenticator("/login", "/login", true)); basicSecurityHandler.setLoginService(new DrillRestLoginService(workManager.getContext())); - - } public void initializeSpnegoAuthentication() throws DrillException { @@ -106,7 +101,6 @@ public void initializeSpnegoAuthentication() throws DrillException { loginService.setIdentityService(identityService); spnegoSecurityHandler.setLoginService(loginService); spnegoSecurityHandler.setConstraintMappings(Collections.emptyList(),knownRoles); - } @Override @@ -118,24 +112,22 @@ public void doStart() throws Exception { if(configuredMechanisms.isEmpty() || configuredMechanisms.contains(HTTP_FORM) ) { basicSecurityHandler.doStart(); } - } - @Override public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest)baseRequest; - String header = req.getHeader(HttpHeader.AUTHORIZATION.asString()); - HttpSession session = req.getSession(true); + + String header = request.getHeader(HttpHeader.AUTHORIZATION.asString()); + HttpSession session = request.getSession(true); SessionAuthentication authentication = (SessionAuthentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); - String uri = req.getRequestURI(); + String uri = request.getRequestURI(); //Before authentication, all requests go through the Formauthenticator except for Spnegologin request //If this authentication is null, user hasn't logged in yet if(authentication == null){ - if( uri.equals("/sn") ){ + if(configuredMechanisms.contains(HTTP_SPNEGO) && configuredMechanisms.contains(HTTP_FORM) && uri.equals("/sn") || configuredMechanisms.contains(HTTP_SPNEGO) && !configuredMechanisms.contains(HTTP_FORM)){ spnegoSecurityHandler.handle(target, baseRequest, request, response); } - else { + else if (configuredMechanisms.contains(HTTP_FORM) || configuredMechanisms.isEmpty()){ basicSecurityHandler.handle(target, baseRequest, request, response); } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java index 3ecfa92f438..6db99805e71 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java @@ -62,9 +62,11 @@ public Authentication validateRequest(ServletRequest request, ServletResponse re HttpSession session = req.getSession(true); Authentication authentication = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); String uri = req.getRequestURI(); + //If the Request URI is for Spnego login then the request is sent to login module using this flag if( uri.equals("/sn")){ mandatory = true; } + //For logout remove the attribute from the session that holds useridentity if (authentication != null && uri.equals("/logout")) { session.removeAttribute("org.eclipse.jetty.security.UserIdentity"); Authentication auth = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); @@ -95,13 +97,13 @@ else if (authentication != null) { UserIdentity user = this.login((String) null, spnegoToken, request); //redirect the request to the desired page after successful login if (user != null) { - String nuri; + String newUri; synchronized(session) { - nuri = (String)session.getAttribute("org.eclipse.jetty.security.form_URI"); - if(nuri == null || nuri.length() == 0) { - nuri = req.getContextPath(); - if(nuri.length() == 0) { - nuri = "/"; + newUri = (String)session.getAttribute("org.eclipse.jetty.security.form_URI"); + if(newUri == null || newUri.length() == 0) { + newUri = req.getContextPath(); + if(newUri.length() == 0) { + newUri = "/"; } } } @@ -110,7 +112,7 @@ else if (authentication != null) { Request base_request = HttpChannel.getCurrentHttpChannel().getRequest(); int redirectCode = base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()?302:303; try { - base_response.sendRedirect(redirectCode, res.encodeRedirectURL(nuri)); + base_response.sendRedirect(redirectCode, res.encodeRedirectURL(newUri)); } catch (IOException e) { e.printStackTrace(); } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java index a7103c30b30..75fbcee4685 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java @@ -41,6 +41,7 @@ public SpnegoUtil(DrillConfig configuration){ this.config = configuration; } + //Reads the SPNEGO realm from the config file public String getSpnegoRealm() throws DrillException { realm = config.getString(ExecConstants.HTTP_SPNEGO_REALM_).trim(); if(realm.isEmpty()){ @@ -49,7 +50,7 @@ public String getSpnegoRealm() throws DrillException { return realm; } - + //Reads the SPNEGO principal from the config file public String getSpnegoPrincipal() throws DrillException { principal = config.getString(ExecConstants.HTTP_SPNEGO_PRINCIPAL_).trim(); if(principal.isEmpty()){ @@ -58,6 +59,7 @@ public String getSpnegoPrincipal() throws DrillException { return principal; } + //Reads the SPNEGO keytab from the config file public String getSpnegoKeytab() throws DrillException { keytab = config.getString(ExecConstants.HTTP_SPNEGO_KEYTAB_).trim(); if(keytab.isEmpty()){ @@ -66,21 +68,23 @@ public String getSpnegoKeytab() throws DrillException { return keytab; } + //Performs the Server login to KDC for SPNEGO public UserGroupInformation getUgi() throws DrillException { + try { if (!UserGroupInformation.isSecurityEnabled()) { - final Configuration Config = new Configuration(); - Config.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, + final Configuration newConfig = new Configuration(); + newConfig.set(CommonConfigurationKeys.HADOOP_SECURITY_AUTHENTICATION, UserGroupInformation.AuthenticationMethod.KERBEROS.toString()); - UserGroupInformation.setConfiguration(Config); - } - try{ - ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal,this.getSpnegoKeytab()); - + UserGroupInformation.setConfiguration(newConfig); + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal, this.getSpnegoKeytab()); + final Configuration oldConfig = new Configuration(); + UserGroupInformation.setConfiguration(oldConfig); + } else { + ugi = UserGroupInformation.loginUserFromKeytabAndReturnUGI(principal, this.getSpnegoKeytab()); } - catch (Exception e){ - throw new DrillException("Login failed for"+principal+"with given keytab"); + }catch (Exception e) { + throw new DrillException("Login failed for" + principal + "with given keytab"); } - return ugi; } } diff --git a/exec/java-exec/src/main/resources/rest/mainlogin.ftl b/exec/java-exec/src/main/resources/rest/mainlogin.ftl index b406d450fff..031cd61dbe5 100644 --- a/exec/java-exec/src/main/resources/rest/mainlogin.ftl +++ b/exec/java-exec/src/main/resources/rest/mainlogin.ftl @@ -14,13 +14,15 @@ <#macro page_body> +
    - Login - SSO - <#if model??> -

    ${model}


    - -
    -
    + Login + SSO + <#if model??> +

    ${model}


    + + + <@page_html/> \ No newline at end of file diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/SpnegoTestUtils.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/SpnegoTestUtils.java new file mode 100644 index 00000000000..d3437ff4bf2 --- /dev/null +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/SpnegoTestUtils.java @@ -0,0 +1,154 @@ +/* + * 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.drill.exec.server; + + +import org.apache.drill.exec.rpc.security.KerberosHelper; +import org.apache.kerby.kerberos.kerb.KrbException; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; + +import java.io.File; +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.file.Files; + +import static org.apache.drill.exec.ExecTest.getTempDir; + +public class SpnegoTestUtils { + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(KerberosHelper.class); + + public File workspace; + + private File kdcDir; + private SimpleKdcServer kdc; + private int kdcPort; + + private final String HOSTNAME = "localhost"; + + public final String CLIENT_SHORT_NAME = "testUser"; + public final String CLIENT_PRINCIPAL; + + public String SERVER_PRINCIPAL; + private final String testName; + + private File keytabDir; + public File clientKeytab; + public File serverKeytab; + + private boolean kdcStarted; + + public SpnegoTestUtils(final String testName) { + final String realm = "EXAMPLE.COM"; + CLIENT_PRINCIPAL = CLIENT_SHORT_NAME + "@" + realm; + + SERVER_PRINCIPAL = "HTTP" + "/" + HOSTNAME + "@" + realm; + this.testName = testName; + } + + public void setupKdc() throws Exception { + kdc = new SimpleKdcServer(); + workspace = new File(getTempDir("spnego_target")); + + kdcDir = new File(workspace, testName); + if(!kdcDir.mkdirs()) { + throw new Exception(String.format("Failed to create the kdc directory %s", kdcDir.getName())); + } + kdc.setWorkDir(kdcDir); + + kdc.setKdcHost(HOSTNAME); + kdcPort = getFreePort(); + kdc.setAllowTcp(true); + kdc.setAllowUdp(false); + kdc.setKdcTcpPort(kdcPort); + + logger.debug("Starting KDC server at {}:{}", HOSTNAME, kdcPort); + + kdc.init(); + kdc.start(); + kdcStarted = true; + + + keytabDir = new File(workspace, testName + "_keytabs"); + if(!keytabDir.mkdirs()) { + throw new Exception(String.format("Failed to create the keytab directory %s", keytabDir.getName())); + } + setupUsers(keytabDir); + + // Kerby sets "java.security.krb5.conf" for us! + System.clearProperty("java.security.auth.login.config"); + System.setProperty("javax.security.auth.useSubjectCredsOnly", "false"); + // Uncomment the following lines for debugging. + // System.setProperty("sun.security.spnego.debug", "true"); + // System.setProperty("sun.security.krb5.debug", "true"); + } + + private int getFreePort() throws IOException { + ServerSocket s = null; + try { + s = new ServerSocket(0); + s.setReuseAddress(true); + return s.getLocalPort(); + } finally { + if (s != null) { + s.close(); + } + } + } + + private void setupUsers(File keytabDir) throws KrbException { + // Create the client user + String clientPrincipal = CLIENT_PRINCIPAL.substring(0, CLIENT_PRINCIPAL.indexOf('@')); + clientKeytab = new File(keytabDir, clientPrincipal.replace('/', '_') + ".keytab"); + logger.debug("Creating {} with keytab {}", clientPrincipal, clientKeytab); + setupUser(kdc, clientKeytab, clientPrincipal); + + // Create the server user + String serverPrincipal = SERVER_PRINCIPAL.substring(0, SERVER_PRINCIPAL.indexOf('@')); + serverKeytab = new File(keytabDir, serverPrincipal.replace('/', '_') + ".keytab"); + logger.debug("Creating {} with keytab {}", SERVER_PRINCIPAL, serverKeytab); + setupUser(kdc, serverKeytab, SERVER_PRINCIPAL); + } + + private void setupUser(SimpleKdcServer kdc, File keytab, String principal) + throws KrbException { + kdc.createPrincipal(principal); + kdc.exportPrincipal(principal, keytab); + } + + public void stopKdc() throws Exception { + if (kdcStarted) { + logger.info("Stopping KDC on {}", kdcPort); + kdc.stop(); + } + + deleteIfExists(clientKeytab); + deleteIfExists(serverKeytab); + deleteIfExists(keytabDir); + deleteIfExists(kdcDir); + deleteIfExists(workspace); + } + + private void deleteIfExists(File file) throws IOException { + if (file != null) { + Files.deleteIfExists(file.toPath()); + } + } +} diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java new file mode 100644 index 00000000000..ec193e9c818 --- /dev/null +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java @@ -0,0 +1,127 @@ +/* + * 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.drill.exec.server; + + +import com.google.common.collect.Lists; +import com.typesafe.config.ConfigValueFactory; +import org.apache.commons.codec.binary.Base64; +import org.apache.drill.BaseTestQuery; +import org.apache.drill.common.config.DrillConfig; +import org.apache.drill.common.config.DrillProperties; +import org.apache.drill.exec.ExecConstants; +import org.apache.drill.exec.rpc.security.KerberosHelper; +import org.apache.drill.exec.rpc.user.security.TestUserBitKerberos; +import org.apache.drill.exec.rpc.user.security.testing.UserAuthenticatorTestImpl; +import org.apache.hadoop.security.authentication.util.KerberosName; +import org.apache.hadoop.security.authentication.util.KerberosUtil; +import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.security.auth.Subject; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.PrivilegedExceptionAction; +import java.util.Properties; +import java.util.concurrent.Callable; + + +public class TestSpnegoAuthentication extends BaseTestQuery { + + private static SpnegoTestUtils spnegoHelper; + + @BeforeClass + public static void setupTest() throws Exception { + spnegoHelper = new SpnegoTestUtils(TestSpnegoAuthentication.class.getSimpleName()); + spnegoHelper.setupKdc(); + + final DrillConfig newConfig = new DrillConfig(DrillConfig.create(cloneDefaultTestConfigProperties()) + .withValue(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS, + ConfigValueFactory.fromIterable(Lists.newArrayList("SPNEGO"))) + .withValue(ExecConstants.HTTP_SPNEGO_PRINCIPAL_, + ConfigValueFactory.fromAnyRef(spnegoHelper.SERVER_PRINCIPAL)) + .withValue(ExecConstants.HTTP_SPNEGO_KEYTAB_, + ConfigValueFactory.fromAnyRef(spnegoHelper.serverKeytab.toString())), + false); + + + final Properties connectionProps = new Properties(); + connectionProps.setProperty(DrillProperties.USER, "anonymous"); + connectionProps.setProperty(DrillProperties.PASSWORD, "anything works!"); + sun.security.krb5.Config.refresh(); + + final Field defaultRealm = KerberosName.class.getDeclaredField("defaultRealm"); + defaultRealm.setAccessible(true); + defaultRealm.set(null, KerberosUtil.getDefaultRealm()); + + updateTestCluster(1, newConfig, connectionProps); + + + } + + @Test + public void testRequestWithAuthorization() throws Exception { + + final Properties connectionProps = new Properties(); + connectionProps.setProperty(DrillProperties.SERVICE_PRINCIPAL, spnegoHelper.SERVER_PRINCIPAL); + connectionProps.setProperty(DrillProperties.KERBEROS_FROM_SUBJECT, "true"); + final Subject clientSubject = JaasKrbUtil.loginUsingKeytab(spnegoHelper.CLIENT_PRINCIPAL, + spnegoHelper.clientKeytab.getAbsoluteFile()); + + String token = Subject.doAs(clientSubject, new PrivilegedExceptionAction() { + @Override + public String run() throws Exception { + + GSSManager gssManager = GSSManager.getInstance(); + GSSContext gssContext = null; + try { + String servicePrincipal = spnegoHelper.SERVER_PRINCIPAL; + Oid oid = KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL"); + GSSName serviceName = gssManager.createName(servicePrincipal, + oid); + oid = KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID"); + gssContext = gssManager.createContext(serviceName, oid, null, + GSSContext.DEFAULT_LIFETIME); + gssContext.requestCredDeleg(true); + gssContext.requestMutualAuth(true); + + byte[] inToken = new byte[0]; + byte[] outToken = gssContext.initSecContext(inToken, 0, inToken.length); + Base64 base64 = new Base64(0); + return base64.encodeToString(outToken); + + } finally { + if (gssContext != null) { + gssContext.dispose(); + } + } + } + }); + + + + } + + +} From 5ff12ba1fe38fd728b0c33a686c665547dd21c50 Mon Sep 17 00:00:00 2001 From: Sindhuri Date: Fri, 15 Sep 2017 15:21:53 -0700 Subject: [PATCH 3/3] Formatted code --- .../org/apache/drill/exec/ExecConstants.java | 5 +- .../server/rest/LogInLogOutResources.java | 15 +++--- .../server/rest/ViewableWithPermissions.java | 4 +- .../drill/exec/server/rest/WebServer.java | 12 ++--- .../auth/DrillConstraintSecurityHandler.java | 8 ++- .../server/rest/auth/DrillErrorHandler.java | 21 ++------ .../rest/auth/DrillSecurityHandler.java | 52 ++++++++++--------- .../rest/auth/DrillSpnegoAuthenticator.java | 33 ++++-------- .../rest/auth/DrillSpnegoLoginService.java | 29 +++-------- .../exec/server/rest/auth/SpnegoUtil.java | 8 --- .../src/main/resources/rest/generic.ftl | 2 +- .../rest/{mainlogin.ftl => mainLogin.ftl} | 4 +- .../exec/server/TestSpnegoAuthentication.java | 32 ++++++++++++ 13 files changed, 102 insertions(+), 123 deletions(-) rename exec/java-exec/src/main/resources/rest/{mainlogin.ftl => mainLogin.ftl} (87%) diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java index dd1e3f7edac..48140eea291 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java @@ -123,9 +123,8 @@ public interface ExecConstants { String HTTP_SESSION_MEMORY_MAXIMUM = "drill.exec.http.session.memory.maximum"; String HTTP_SESSION_MAX_IDLE_SECS = "drill.exec.http.session_max_idle_secs"; String HTTP_AUTHENTICATION_MECHANISMS = "drill.exec.http.auth.mechanisms"; - String HTTP_SPNEGO_PRINCIPAL_= "drill.exec.http.spnego.auth.principal"; - String HTTP_SPNEGO_KEYTAB_= "drill.exec.http.spnego.auth.keytab"; - String HTTP_SPNEGO_REALM_= "drill.exec.http.spnego.auth.realm"; + String HTTP_SPNEGO_PRINCIPAL_= "drill.exec.http.auth.spnego.principal"; + String HTTP_SPNEGO_KEYTAB_= "drill.exec.http.auth.spnego.keytab"; String HTTP_KEYSTORE_PATH = "drill.exec.ssl.keyStorePath"; String HTTP_KEYSTORE_PASSWORD = "drill.exec.ssl.keyStorePassword"; String HTTP_TRUSTSTORE_PATH = "drill.exec.ssl.trustStorePath"; diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java index f30e62fe4bd..af765d888d9 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/LogInLogOutResources.java @@ -44,7 +44,7 @@ @PermitAll public class LogInLogOutResources { public static final String REDIRECT_QUERY_PARM = "redirect"; - public static final String LOGIN_RESOURCE = "mainlogin"; + public static final String LOGIN_RESOURCE = "mainLogin"; @GET @Path("/login") @@ -70,17 +70,16 @@ public Viewable getLoginPage(@Context HttpServletRequest request, @Context HttpS } @GET - @Path("/sn") + @Path("/spnegoLogin") @Produces(MediaType.TEXT_HTML) - public Viewable getspnegologin(@Context HttpServletRequest request, @Context HttpServletResponse response, + public Viewable getSpnegologin(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) throws Exception { if (AuthDynamicFeature.isUserLoggedIn(sc)) { request.getRequestDispatcher("/").forward(request, response); return null; } - - return ViewableWithPermissions.createmainLoginPage("Invalid SPNEGO credentials."); + return ViewableWithPermissions.createMainLoginPage("Invalid SPNEGO credentials or SPNEGO is not configured"); } // Request type is POST because POST request which contains the login credentials are invalid and the request is @@ -103,12 +102,11 @@ public void logout(@Context HttpServletRequest req, @Context HttpServletResponse req.getRequestDispatcher("/").forward(req, resp); } @GET - @Path("/mainlogin") + @Path("/mainLogin") @Produces(MediaType.TEXT_HTML) public Viewable getMainLoginPage(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context SecurityContext sc, @Context UriInfo uriInfo, @QueryParam(REDIRECT_QUERY_PARM) String redirect) throws Exception { - //req.getRequestDispatcher("/").forward(req, resp); if (!StringUtils.isEmpty(redirect)) { // If the URL has redirect in it, set the redirect URI in session, so that after the login is successful, request // is forwarded to the redirect page. @@ -116,8 +114,7 @@ public Viewable getMainLoginPage(@Context HttpServletRequest request, @Context H final URI destURI = UriBuilder.fromUri(URLDecoder.decode(redirect, "UTF-8")).build(); session.setAttribute(FormAuthenticator.__J_URI, destURI.toString()); } - return ViewableWithPermissions.createmainLoginPage(null); + return ViewableWithPermissions.createMainLoginPage(null); } - } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java index fb56cfd0ab0..00a057defae 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/ViewableWithPermissions.java @@ -61,8 +61,8 @@ public static Viewable createLoginPage(final String errorMsg) { return new ViewableWithPermissions(true, "/rest/login.ftl", null, false, errorMsg); } - public static Viewable createmainLoginPage(final String errorMsg) { - return new ViewableWithPermissions(true, "/rest/mainlogin.ftl", null, false, errorMsg); + public static Viewable createMainLoginPage(final String errorMsg) { + return new ViewableWithPermissions(true, "/rest/mainLogin.ftl", null, false, errorMsg); } private ViewableWithPermissions(final boolean authEnabled, final String templateName, final SecurityContext sc, 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 afe4ba843ce..f87f6af2e5a 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 @@ -31,6 +31,7 @@ import org.apache.drill.exec.exception.DrillbitStartupException; import org.apache.drill.exec.rpc.security.plain.PlainFactory; import org.apache.drill.exec.server.BootStrapContext; +import org.apache.drill.exec.server.rest.auth.DrillErrorHandler; import org.apache.drill.exec.server.rest.auth.DrillRestLoginService; import org.apache.drill.exec.server.rest.auth.DrillSecurityHandler; import org.apache.drill.exec.work.WorkManager; @@ -104,8 +105,6 @@ public class WebServer implements AutoCloseable { private final BootStrapContext context; - private final String SpnegoAuth = "SPNEGO"; - /** * Create Jetty based web server. * @@ -147,8 +146,8 @@ public void start() throws Exception { if (embeddedJetty == null) { return; } - boolean authEnabled = config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED); - if (authEnabled && !context.getAuthProvider().containsFactory(PlainFactory.SIMPLE_NAME)) { + boolean authEnabled = (config.getBoolean(ExecConstants.USER_AUTHENTICATION_ENABLED)); + if (authEnabled && !context.getAuthProvider().containsFactory(PlainFactory.SIMPLE_NAME)) { logger.warn("Not starting web server. Currently Drill supports web authentication only through " + "username/password. But PLAIN mechanism is not configured."); return; @@ -167,7 +166,7 @@ public void start() throws Exception { embeddedJetty.addConnector(serverConnector); // Add resources - final ErrorHandler errorHandler = new ErrorHandler(); + final DrillErrorHandler errorHandler = new DrillErrorHandler(); errorHandler.setShowStacks(true); errorHandler.setShowMessageInTitle(true); @@ -195,8 +194,7 @@ public void start() throws Exception { if (authEnabled) { //DrillSecurityHandler is used to support SPNEGO and FORM authentication together - DrillSecurityHandler securityHandler = new DrillSecurityHandler(config, workManager); - servletContextHandler.setSecurityHandler(securityHandler); + servletContextHandler.setSecurityHandler(new DrillSecurityHandler(config, workManager.getContext())); servletContextHandler.setSessionHandler(createSessionHandler(servletContextHandler.getSecurityHandler())); } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java index c0cff32d4ba..5edb7676a76 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillConstraintSecurityHandler.java @@ -19,12 +19,9 @@ import org.eclipse.jetty.security.ConstraintSecurityHandler; - +/** Class that extends the ConstraintSecurityHandler that helps to initiate two both the Form and SecurityHandlers**/ public class DrillConstraintSecurityHandler extends ConstraintSecurityHandler { - public DrillConstraintSecurityHandler() { - - } @Override public void doStart() throws Exception { @@ -35,4 +32,5 @@ public void doStart() throws Exception { public void doStop() throws Exception { super.doStop(); } -} + +} \ No newline at end of file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java index da9e42aa141..e5634001f98 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillErrorHandler.java @@ -15,38 +15,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - - - package org.apache.drill.exec.server.rest.auth; -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.ErrorHandler; - import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.Writer; public class DrillErrorHandler extends ErrorHandler { - - @Override protected void writeErrorPageMessage(HttpServletRequest request, Writer writer, int code, String message, String uri) throws IOException { super.writeErrorPageMessage(request,writer,code,message,uri); - //writer.write("

    Spnego login failed

    "); - String path = request.getContextPath(); - String ref = path+"/login"; - writer.write(" root "); + writer.write("

    Check the requirements or Use this link to do Form Authentication

    "); + writer.write(" login "); } } -} +} \ No newline at end of file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java index 5ce36a84b17..d85a9164924 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSecurityHandler.java @@ -25,6 +25,8 @@ import org.apache.drill.common.exceptions.DrillException; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.exception.DrillbitStartupException; +import org.apache.drill.exec.rpc.security.AuthStringUtil; +import org.apache.drill.exec.server.DrillbitContext; import org.apache.drill.exec.work.WorkManager; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.security.ConstraintMapping; @@ -52,28 +54,28 @@ public class DrillSecurityHandler extends ConstraintSecurityHandler { - private final WorkManager workManager; private DrillConstraintSecurityHandler basicSecurityHandler; private DrillConstraintSecurityHandler spnegoSecurityHandler; + private Set knownRoles = ImmutableSet.of(AUTHENTICATED_ROLE, ADMIN_ROLE); private final String HTTP_FORM ="FORM"; private final String HTTP_SPNEGO ="SPNEGO"; - private List configuredMechanisms = Lists.newArrayList(); - private Set knownRoles = ImmutableSet.of(AUTHENTICATED_ROLE, ADMIN_ROLE); + private List configuredMechanisms = Lists.newArrayList(); private SpnegoUtil spnUtil; + private boolean spnegoEnabled = false; + private boolean formEnabled = false; - public DrillSecurityHandler(DrillConfig config,WorkManager work) throws DrillbitStartupException,IOException { + public DrillSecurityHandler(DrillConfig config,DrillbitContext drillContext) throws DrillbitStartupException,IOException { spnUtil = new SpnegoUtil(config); - this.workManager = work; try { if (config.hasPath(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS)) { configuredMechanisms = config.getStringList(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS); - if (configuredMechanisms.contains(HTTP_FORM)) { - initializeFormAuthentication(); - + if (AuthStringUtil.listContains(configuredMechanisms,HTTP_FORM)) { + formEnabled = true; + initializeFormAuthentication(drillContext); } - if (configuredMechanisms.contains(HTTP_SPNEGO)) { - initializeSpnegoAuthentication(); - + if (AuthStringUtil.listContains(configuredMechanisms,HTTP_SPNEGO)) { + spnegoEnabled = true ; + initializeSpnegoAuthentication(drillContext); } } } @@ -82,21 +84,22 @@ public DrillSecurityHandler(DrillConfig config,WorkManager work) throws Drillbit } //Backward compatability for Form authentication if(!config.hasPath(ExecConstants.HTTP_AUTHENTICATION_MECHANISMS)){ - initializeFormAuthentication(); + formEnabled = true; + initializeFormAuthentication(drillContext); } } - public void initializeFormAuthentication(){ + public void initializeFormAuthentication(DrillbitContext drillContext){ basicSecurityHandler = new DrillConstraintSecurityHandler(); basicSecurityHandler.setConstraintMappings(Collections.emptyList(), knownRoles); basicSecurityHandler.setAuthenticator(new FormAuthenticator("/login", "/login", true)); - basicSecurityHandler.setLoginService(new DrillRestLoginService(workManager.getContext())); + basicSecurityHandler.setLoginService(new DrillRestLoginService(drillContext)); } - public void initializeSpnegoAuthentication() throws DrillException { + public void initializeSpnegoAuthentication(DrillbitContext drillContext) throws DrillException { spnegoSecurityHandler = new DrillConstraintSecurityHandler(); spnegoSecurityHandler.setAuthenticator(new DrillSpnegoAuthenticator()); - final SpnegoLoginService loginService = new DrillSpnegoLoginService(spnUtil.getSpnegoRealm(),spnUtil.getSpnegoPrincipal(),workManager.getContext(),spnUtil.getUgi()); + final SpnegoLoginService loginService = new DrillSpnegoLoginService(spnUtil,drillContext); final IdentityService identityService = new DefaultIdentityService(); loginService.setIdentityService(identityService); spnegoSecurityHandler.setLoginService(loginService); @@ -106,10 +109,10 @@ public void initializeSpnegoAuthentication() throws DrillException { @Override public void doStart() throws Exception { super.doStart(); - if(!configuredMechanisms.isEmpty() && configuredMechanisms.contains(HTTP_SPNEGO)) { + if(!configuredMechanisms.isEmpty() && spnegoEnabled) { spnegoSecurityHandler.doStart(); } - if(configuredMechanisms.isEmpty() || configuredMechanisms.contains(HTTP_FORM) ) { + if(formEnabled) { basicSecurityHandler.doStart(); } } @@ -124,20 +127,19 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques //Before authentication, all requests go through the Formauthenticator except for Spnegologin request //If this authentication is null, user hasn't logged in yet if(authentication == null){ - if(configuredMechanisms.contains(HTTP_SPNEGO) && configuredMechanisms.contains(HTTP_FORM) && uri.equals("/sn") || configuredMechanisms.contains(HTTP_SPNEGO) && !configuredMechanisms.contains(HTTP_FORM)){ + if(spnegoEnabled && uri.equals("/sn") || !formEnabled){ spnegoSecurityHandler.handle(target, baseRequest, request, response); } - else if (configuredMechanisms.contains(HTTP_FORM) || configuredMechanisms.isEmpty()){ + else if (formEnabled){ basicSecurityHandler.handle(target, baseRequest, request, response); } - } //If user has logged in, use the corresponding handler to handle the request else{ - if(authentication.getAuthMethod() == "FORM"){ + if(authentication.getAuthMethod() == HTTP_FORM){ basicSecurityHandler.handle(target, baseRequest, request, response); } - else if(authentication.getAuthMethod() == "SPNEGO"){ + else if(authentication.getAuthMethod() == HTTP_SPNEGO){ spnegoSecurityHandler.handle(target, baseRequest, request, response); } } @@ -146,10 +148,10 @@ else if(authentication.getAuthMethod() == "SPNEGO"){ @Override public void setHandler(Handler handler) { super.setHandler(handler); - if(!configuredMechanisms.isEmpty() && configuredMechanisms.contains(HTTP_SPNEGO)) { + if(!configuredMechanisms.isEmpty() && spnegoEnabled) { spnegoSecurityHandler.setHandler(handler); } - if(configuredMechanisms.isEmpty() || configuredMechanisms.contains(HTTP_SPNEGO)) { + if(formEnabled) { basicSecurityHandler.setHandler(handler); } } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java index 6db99805e71..9aef671de72 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoAuthenticator.java @@ -47,12 +47,8 @@ import java.io.IOException; public class DrillSpnegoAuthenticator extends SpnegoAuthenticator { - private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillSpnegoAuthenticator.class); - - public DrillSpnegoAuthenticator() { - - } + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(DrillSpnegoAuthenticator.class); @Override public Authentication validateRequest(ServletRequest request, ServletResponse response, boolean mandatory) throws ServerAuthException { @@ -62,8 +58,8 @@ public Authentication validateRequest(ServletRequest request, ServletResponse re HttpSession session = req.getSession(true); Authentication authentication = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); String uri = req.getRequestURI(); - //If the Request URI is for Spnego login then the request is sent to login module using this flag - if( uri.equals("/sn")){ + //If the Request URI is for Spnego login then the request is sent to the login module my modifying this flag + if (uri.equals("/spnegoLogin")) { mandatory = true; } //For logout remove the attribute from the session that holds useridentity @@ -71,11 +67,9 @@ public Authentication validateRequest(ServletRequest request, ServletResponse re session.removeAttribute("org.eclipse.jetty.security.UserIdentity"); Authentication auth = (Authentication) session.getAttribute("org.eclipse.jetty.security.UserIdentity"); return auth; - } - else if (authentication != null) { + } else if (authentication != null) { return authentication; - } - else { + } else { String header = req.getHeader(HttpHeader.AUTHORIZATION.asString()); if (!mandatory) { return new DeferredAuthentication(this); @@ -98,11 +92,11 @@ else if (authentication != null) { //redirect the request to the desired page after successful login if (user != null) { String newUri; - synchronized(session) { - newUri = (String)session.getAttribute("org.eclipse.jetty.security.form_URI"); - if(newUri == null || newUri.length() == 0) { + synchronized (session) { + newUri = (String) session.getAttribute("org.eclipse.jetty.security.form_URI"); + if (newUri == null || newUri.length() == 0) { newUri = req.getContextPath(); - if(newUri.length() == 0) { + if (newUri.length() == 0) { newUri = "/"; } } @@ -110,7 +104,7 @@ else if (authentication != null) { response.setContentLength(0); Response base_response = HttpChannel.getCurrentHttpChannel().getResponse(); Request base_request = HttpChannel.getCurrentHttpChannel().getRequest(); - int redirectCode = base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion()?302:303; + int redirectCode = base_request.getHttpVersion().getVersion() < HttpVersion.HTTP_1_1.getVersion() ? 302 : 303; try { base_response.sendRedirect(redirectCode, res.encodeRedirectURL(newUri)); } catch (IOException e) { @@ -118,13 +112,10 @@ else if (authentication != null) { } return new UserAuthentication(this.getAuthMethod(), user); } - } - return Authentication.UNAUTHENTICATED; } } - } @@ -138,6 +129,4 @@ public UserIdentity login(String username, Object password, ServletRequest reque return user; } - - -} +} \ No newline at end of file diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java index 0e6e8108412..e945ba9fb64 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/DrillSpnegoLoginService.java @@ -21,6 +21,7 @@ import org.apache.drill.common.config.DrillConfig; +import org.apache.drill.common.exceptions.DrillException; import org.apache.drill.exec.ExecConstants; import org.apache.drill.exec.expr.fn.impl.ContextFunctions; import org.apache.drill.exec.server.DrillbitContext; @@ -58,19 +59,17 @@ public class DrillSpnegoLoginService extends SpnegoLoginService { - public DrillSpnegoLoginService(String realm, String serverPrincipal, DrillbitContext drillbitContext, UserGroupInformation ugSpnego) { - super(realm); - this.serverPrincipal = Objects.requireNonNull(serverPrincipal); - drillContext = drillbitContext; - ugi = ugSpnego; + public DrillSpnegoLoginService(SpnegoUtil spnegoUtil, DrillbitContext drillBitContext) throws DrillException { + super(DrillSpnegoLoginService.class.getName()); + this.serverPrincipal = spnegoUtil.getSpnegoPrincipal(); + drillContext = drillBitContext; + ugi = spnegoUtil.getUgi(); } @Override protected void doStart() throws Exception { // Override the parent implementation, setting _targetName to be the serverPrincipal // without the need for a one-line file to do the same thing. - // - // AbstractLifeCycle's doStart() method does nothing, so we aren't missing any extra logic. final Field targetNameField = SpnegoLoginService.class.getDeclaredField(TARGET_NAME_FIELD_NAME); targetNameField.setAccessible(true); targetNameField.set(this, serverPrincipal); @@ -99,7 +98,7 @@ public UserIdentity spnegologin(String username, Object credentials) { GSSManager manager = GSSManager.getInstance(); try { - + //Providing both OID's is required here. If we provide only one, we're requiring that clients provide us the SPNEGO OID to authentica via Kerberos. Oid spnegoOid = new Oid("1.3.6.1.5.5.2"); Oid krb5Oid = new Oid("1.2.840.113554.1.2.2"); GSSName gssName = manager.createName(serverPrincipal, null); @@ -116,10 +115,6 @@ public UserIdentity spnegologin(String username, Object credentials) { if (gContext.isEstablished()) { String clientName = gContext.getSrcName().toString(); String role = clientName.substring(0, clientName.indexOf(64)); - // LOG.debug("SpnegoUserRealm: established a security context", new Object[0]); - //LOG.debug("Client Principal is: " + gContext.getSrcName(), new Object[0]); - // LOG.debug("Server Principal is: " + gContext.getTargName(), new Object[0]); - // LOG.debug("Client Default Role: " + role, new Object[0]); final SystemOptionManager sysOptions = drillContext.getOptionManager(); final boolean isAdmin = ImpersonationUtil.hasAdminPrivileges(role, @@ -138,17 +133,7 @@ public UserIdentity spnegologin(String username, Object credentials) { } catch (GSSException gsse) { logger.warn("Caught GSSException trying to authenticate the client", gsse); } - return null; - } - - @Override - public boolean validate(UserIdentity user) { - return true; - } - - - } diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java index 75fbcee4685..7d22d17a56f 100644 --- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java +++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/rest/auth/SpnegoUtil.java @@ -41,14 +41,6 @@ public SpnegoUtil(DrillConfig configuration){ this.config = configuration; } - //Reads the SPNEGO realm from the config file - public String getSpnegoRealm() throws DrillException { - realm = config.getString(ExecConstants.HTTP_SPNEGO_REALM_).trim(); - if(realm.isEmpty()){ - throw new DrillException(" drill.exec.http.spnego.auth.realm in the configuration file can't be empty"); - } - return realm; - } //Reads the SPNEGO principal from the config file public String getSpnegoPrincipal() throws DrillException { diff --git a/exec/java-exec/src/main/resources/rest/generic.ftl b/exec/java-exec/src/main/resources/rest/generic.ftl index a7cee1d7034..d04414fec22 100644 --- a/exec/java-exec/src/main/resources/rest/generic.ftl +++ b/exec/java-exec/src/main/resources/rest/generic.ftl @@ -77,7 +77,7 @@
  • Documentation <#if showLogin == true > -
  • Log In +
  • Log In <#if showLogout == true >
  • Log Out (${loggedInUserName}) diff --git a/exec/java-exec/src/main/resources/rest/mainlogin.ftl b/exec/java-exec/src/main/resources/rest/mainLogin.ftl similarity index 87% rename from exec/java-exec/src/main/resources/rest/mainlogin.ftl rename to exec/java-exec/src/main/resources/rest/mainLogin.ftl index 031cd61dbe5..de14ae84cf4 100644 --- a/exec/java-exec/src/main/resources/rest/mainlogin.ftl +++ b/exec/java-exec/src/main/resources/rest/mainLogin.ftl @@ -17,8 +17,8 @@
    - Login - SSO + Login using FORM AUTHENTICATION + Login using SPNEGO <#if model??>

    ${model}


    diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java b/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java index ec193e9c818..dc0f9e310f5 100644 --- a/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java +++ b/exec/java-exec/src/test/java/org/apache/drill/exec/server/TestSpnegoAuthentication.java @@ -28,17 +28,35 @@ import org.apache.drill.exec.rpc.security.KerberosHelper; import org.apache.drill.exec.rpc.user.security.TestUserBitKerberos; import org.apache.drill.exec.rpc.user.security.testing.UserAuthenticatorTestImpl; +import org.apache.drill.exec.server.rest.auth.DrillSecurityHandler; +import org.apache.drill.exec.server.rest.auth.DrillSpnegoAuthenticator; +import org.apache.drill.exec.server.rest.auth.DrillSpnegoLoginService; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; import org.apache.hadoop.security.authentication.util.KerberosName; import org.apache.hadoop.security.authentication.util.KerberosUtil; import org.apache.kerby.kerberos.kerb.client.JaasKrbUtil; +import org.eclipse.jetty.security.ConstraintSecurityHandler; +import org.eclipse.jetty.security.RoleInfo; +import org.eclipse.jetty.security.SecurityHandler; +import org.eclipse.jetty.server.Authentication; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.server.UserIdentity; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.junit.BeforeClass; import org.junit.Test; +import org.mockito.Mockito; import javax.security.auth.Subject; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.lang.reflect.Field; import java.net.HttpURLConnection; import java.net.URL; @@ -119,6 +137,20 @@ public String run() throws Exception { } }); + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + Mockito.when(request.getHeader(KerberosAuthenticator.AUTHORIZATION)) + .thenReturn(KerberosAuthenticator.NEGOTIATE + " " + token); + Mockito.when(request.getServerName()).thenReturn("localhost"); + + + + DrillSpnegoAuthenticator authenticator = new DrillSpnegoAuthenticator(); + ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler(); + securityHandler.setAuthenticator(authenticator); + Authentication authToken = authenticator.validateRequest((ServletRequest)request,(ServletResponse) response,true); + }