diff --git a/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosAuthenticationHandler.java b/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosAuthenticationHandler.java new file mode 100644 index 0000000..8c73717 --- /dev/null +++ b/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosAuthenticationHandler.java @@ -0,0 +1,682 @@ +package org.apache.hadoop.yarn.ats; + +/** + * Licensed 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. See accompanying LICENSE file. + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.URLEncoder; +import java.util.Calendar; +import java.util.Hashtable; +import java.util.Properties; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.security.auth.Subject; +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.client.KerberosAuthenticator; +import org.apache.commons.codec.binary.Base64; +import org.apache.hadoop.security.authentication.server.AuthenticationHandler; +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.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.Oid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.apache.hadoop.util.PlatformName.IBM_JAVA; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + + + + /** + * The {@link AltKerberosAuthenticationHandler} behaves exactly the same way as + * the {@link KerberosAuthenticationHandler}, except that it allows for an + * alternative form of authentication for browsers while still using Kerberos + * for Java access. This is an abstract class that should be subclassed + * to allow a developer to implement their own custom authentication for browser + * access. The alternateAuthenticate method will be called whenever a request + * comes from a browser. + *

+ */ +public class AltATSLdapKerberosAuthenticationHandler + implements AuthenticationHandler { + + private static Logger LOG = LoggerFactory.getLogger(AltATSLdapKerberosAuthenticationHandler.class); + + /** + * Constant that identifies the authentication mechanism. + */ + public static final String TYPE = "kerberos"; + + /** + * Constant for the configuration property that indicates which user agents + * are not considered browsers (comma separated) + */ + public static final String NON_BROWSER_USER_AGENTS = + "alt-kerberos.non-browser.user-agents"; + private static final String NON_BROWSER_USER_AGENTS_DEFAULT = + "java,curl,wget,perl"; + + private String[] nonBrowserUserAgents; + + + /** + * Constant for the configuration property that indicates the kerberos principal. + */ + public static final String PRINCIPAL = TYPE + ".principal"; + + /** + * Constant for the configuration property that indicates the keytab file path. + */ + public static final String KEYTAB = TYPE + ".keytab"; + + /** + * Constant for the configuration property that indicates the Kerberos name + * rules for the Kerberos principals. + */ + public static final String NAME_RULES = TYPE + ".name.rules"; + + /** + * HTTP header used by the LDAPAuth server endpoint during an authentication sequence. + */ + public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + + /** + * HTTP header used by the SPNEGO client endpoint during an authentication sequence. + */ + public static final String AUTHORIZATION = "Authorization"; + + /** + * HTTP header prefix used by the SPNEGO client/server endpoints during an authentication sequence. + */ + + private static final String BASIC_AUTH = "BASIC realm=\"Hadoop Auth\""; + + private static String ldapUrl = null; + + private static String baseDn = null; + + private static String ldapDomain = null; + private static String bindIdDn = null; + private static String bindPasswd = null; + + + public static final String REDIRECT_URL = + "alt-kerberos.redirect.url"; + + public static final String LDAP_URL = + "alt-kerberos.ldap.url"; + + public static final String LDAP_DOMAIN = + "alt-kerberos.ldap.domain"; + + public static final String LDAP_BASEDN = + "alt-kerberos.ldap.basedn"; + + public static final String LDAPBINDDN = + "alt-kerberos.ldap.binddn"; + public static final String LDAPBINDPASSWD = + "alt-kerberos.ldap.bindpasswd"; + + /** + * Kerberos context configuration for the JDK GSS library. + */ + private static class KerberosConfiguration extends Configuration { + private String keytab; + private String principal; + + public KerberosConfiguration(String keytab, String principal) { + this.keytab = keytab; + this.principal = principal; + } + + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map options = new HashMap(); + if (IBM_JAVA) { + options.put("useKeytab", + keytab.startsWith("file://") ? keytab : "file://" + keytab); + options.put("principal", principal); + options.put("credsType", "acceptor"); + } else { + options.put("keyTab", keytab); + options.put("principal", principal); + options.put("useKeyTab", "true"); + options.put("storeKey", "true"); + options.put("doNotPrompt", "true"); + options.put("useTicketCache", "true"); + options.put("renewTGT", "true"); + options.put("isInitiator", "false"); + } + options.put("refreshKrb5Config", "true"); + String ticketCache = System.getenv("KRB5CCNAME"); + if (ticketCache != null) { + if (IBM_JAVA) { + options.put("useDefaultCcache", "true"); + // The first value searched when "useDefaultCcache" is used. + System.setProperty("KRB5CCNAME", ticketCache); + options.put("renewTGT", "true"); + options.put("credsType", "both"); + } else { + options.put("ticketCache", ticketCache); + } + } + if (LOG.isDebugEnabled()) { + options.put("debug", "true"); + } + + return new AppConfigurationEntry[]{ + new AppConfigurationEntry(KerberosUtil.getKrb5LoginModuleName(), + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options),}; + } + } + + + + private String type; + private String keytab; + private GSSManager gssManager; + private Subject serverSubject = new Subject(); + private List loginContexts = new ArrayList(); + + /** + * Creates a Kerberos SPNEGO authentication handler with the default + * auth-token type, kerberos. + */ + public AltATSLdapKerberosAuthenticationHandler() { + this(TYPE); + } + + /** + * Creates a Kerberos SPNEGO authentication handler with a custom auth-token + * type. + * + * @param type auth-token type. + */ + public AltATSLdapKerberosAuthenticationHandler(String type) { + this.type = type; + } + + /** + * Initializes the authentication handler instance. + *

+ * It creates a Kerberos context using the principal and keytab specified in the configuration. + *

+ * This method is invoked by the {@link AuthenticationFilter#init} method. + * + * @param config configuration properties to initialize the handler. + * + * @throws ServletException thrown if the handler could not be initialized. + */ + + //@Override + public void init(Properties config) throws ServletException { + try { + nonBrowserUserAgents = config.getProperty( + NON_BROWSER_USER_AGENTS, NON_BROWSER_USER_AGENTS_DEFAULT) + .split("\\W*,\\W*"); + for (int i = 0; i < nonBrowserUserAgents.length; i++) { + nonBrowserUserAgents[i] = + nonBrowserUserAgents[i].toLowerCase(Locale.ENGLISH); + } + + String principal = config.getProperty(PRINCIPAL); + if (principal == null || principal.trim().length() == 0) { + throw new ServletException("Principal not defined in configuration"); + } + keytab = config.getProperty(KEYTAB, keytab); + if (keytab == null || keytab.trim().length() == 0) { + throw new ServletException("Keytab not defined in configuration"); + } + if (!new File(keytab).exists()) { + throw new ServletException("Keytab does not exist: " + keytab); + } + + // use all SPNEGO principals in the keytab if a principal isn't + // specifically configured + final String[] spnegoPrincipals; + if (principal.equals("*")) { + spnegoPrincipals = KerberosUtil.getPrincipalNames( + keytab, Pattern.compile("HTTP/.*")); + if (spnegoPrincipals.length == 0) { + throw new ServletException("Principals do not exist in the keytab"); + } + } else { + spnegoPrincipals = new String[]{principal}; + } + + String nameRules = config.getProperty(NAME_RULES, null); + if (nameRules != null) { + KerberosName.setRules(nameRules); + } + + for (String spnegoPrincipal : spnegoPrincipals) { + LOG.info("Login using keytab {}, for principal {}", + keytab, spnegoPrincipal); + final KerberosConfiguration kerberosConfiguration = + new KerberosConfiguration(keytab, spnegoPrincipal); + final LoginContext loginContext = + new LoginContext("", serverSubject, null, kerberosConfiguration); + try { + loginContext.login(); + } catch (LoginException le) { + LOG.warn("Failed to login as [{}]", spnegoPrincipal, le); + throw new AuthenticationException(le); + } + loginContexts.add(loginContext); + } + try { + gssManager = Subject.doAs(serverSubject, new PrivilegedExceptionAction() { + + //@Override + public GSSManager run() throws Exception { + return GSSManager.getInstance(); + } + }); + } catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } catch (Exception ex) { + throw new ServletException(ex); + } + ldapDomain = config.getProperty(LDAP_DOMAIN); + baseDn = config.getProperty(LDAP_BASEDN); + ldapUrl = config.getProperty(LDAP_URL); + } + /** + * Subclasses should implement this method to provide the custom + * authentication to be used for browsers. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * @return an authentication token if the request is authorized, or null + * @throws IOException thrown if an IO error occurs + * @throws AuthenticationException thrown if an authentication error occurs + */ + //@Override + public AuthenticationToken alternateAuthenticate(HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token = null; + LOG.info("AlternateAuthenticator"); + String userName="testid"; + String upnIn = "testid@EXAMPLE.COM"; + token = new AuthenticationToken(userName, upnIn, getType()); + return token; + } + + + + + public String[] simpleBindSearch (String username, String ldapurl, String ldapDomain, String searchDn, String bindIdDn, String bindPasswd) throws AuthenticationException { + Hashtable env = new Hashtable(11); + + String baseDN = new String(); + + NamingEnumeration results = null; + + InitialDirContext ctx = null; + Attributes attrs = null; + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + // Specify SSL + + //env.put(Context.SECURITY_PROTOCOL, "ssl"); + env.put(Context.PROVIDER_URL, ldapurl); + + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, bindIdDn); + env.put(Context.SECURITY_CREDENTIALS, bindPasswd); + + + try { + ctx = new InitialDirContext(env); + } catch (NamingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw new AuthenticationException("Unable to parse authentication cookie"); + + } + SearchControls constraints = new SearchControls(); + constraints.setSearchScope(SearchControls.SUBTREE_SCOPE); + String filter = "(cn="+username+")"; + try { + results = ctx.search(searchDn, filter, constraints); + } catch (NamingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw new AuthenticationException("Unable to parse authentication cookie"); + + } + + String uSNCreated = null; + String upn = null; + String[] dataArray = new String[2]; + + try { + while (results.hasMore()) { + SearchResult res = (SearchResult) results.next(); + upn = (String) res.getAttributes().get("userPrincipalName").get(); + uSNCreated = (String) res.getAttributes().get("uSNCreated").get(); + + + } + dataArray[0] = upn; + dataArray[1] = uSNCreated; + ctx.close(); + + } catch (NamingException e) { + // TODO Auto-generated catch block + throw new AuthenticationException("Unable to parse authentication cookie"); + + + + + } finally { + try { + ctx.close(); + } catch (NamingException e) { + // TODO Auto-generated catch block + throw new AuthenticationException("Unable to parse authentication cookie"); + + } + + } + return dataArray; + } + + + + + /** + * Releases any resources initialized by the authentication handler. + *

+ * It destroys the Kerberos context. + */ + //@Override + public void destroy() { + keytab = null; + serverSubject = null; + for (LoginContext loginContext : loginContexts) { + try { + loginContext.logout(); + } catch (LoginException ex) { + LOG.warn(ex.getMessage(), ex); + } + } + loginContexts.clear(); + } + + /** + * Returns the authentication type of the authentication handler, 'kerberos'. + *

+ * + * @return the authentication type of the authentication handler, 'kerberos'. + */ + //@Override + public String getType() { + return type; + } + + /** + * Returns the Kerberos principals used by the authentication handler. + * + * @return the Kerberos principals used by the authentication handler. + */ + protected Set getPrincipals() { + return serverSubject.getPrincipals(KerberosPrincipal.class); + } + + /** + * Returns the keytab used by the authentication handler. + * + * @return the keytab used by the authentication handler. + */ + protected String getKeytab() { + return keytab; + } + + /** + * This is an empty implementation, it always returns TRUE. + * + * + * + * @param token the authentication token if any, otherwise NULL. + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return TRUE + * @throws IOException it is never thrown. + * @throws AuthenticationException it is never thrown. + */ + //@Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + return true; + } + + /** + * It enforces the the Kerberos SPNEGO authentication sequence returning an {@link AuthenticationToken} only + * after the Kerberos SPNEGO sequence has completed successfully. + * + * @param request the HTTP client request. + * @param response the HTTP client response. + * + * @return an authentication token if the Kerberos SPNEGO sequence is complete and valid, + * null if it is in progress (in this case the handler handles the response to the client). + * + * @throws IOException thrown if an IO error occurred. + * @throws AuthenticationException thrown if Kerberos SPNEGO sequence failed. + */ + //@Override + public AuthenticationToken authenticate(HttpServletRequest request, final HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token = null; + if (isBrowser(request.getHeader("User-Agent"))) { + LOG.info("AltKerberos Browser Authentication"); + token = alternateAuthenticate(request, response); + + return token; + } + LOG.info("Kerberos Normal Authentication"); + + + String authorization = request.getHeader(KerberosAuthenticator.AUTHORIZATION); + + if (authorization == null || !authorization.startsWith(KerberosAuthenticator.NEGOTIATE)) { + response.setHeader(WWW_AUTHENTICATE, KerberosAuthenticator.NEGOTIATE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + if (authorization == null) { + LOG.info("SPNEGO starting"); + } else { + LOG.info("'" + KerberosAuthenticator.AUTHORIZATION + "' does not start with '" + + KerberosAuthenticator.NEGOTIATE + "' : {}", authorization); + } + } else { + authorization = authorization.substring(KerberosAuthenticator.NEGOTIATE.length()).trim(); + final Base64 base64 = new Base64(0); + final byte[] clientToken = base64.decode(authorization); + final String serverName = request.getServerName(); + LOG.info("Run Initial serverName Object: "+serverName); + LOG.info("Run Initial clientToken Object: "+clientToken); + + + try { + LOG.info(serverSubject.getPrincipals().toString()); + for ( Principal principal : serverSubject.getPrincipals()) { + LOG.info(principal.getName()); + } + token = Subject.doAs(serverSubject, new PrivilegedExceptionAction() { + + //@Override + public AuthenticationToken run() throws Exception { + AuthenticationToken token = null; + GSSContext gssContext = null; + GSSCredential gssCreds = null; + try { + LOG.info("RUN ServerName: "+serverName); + LOG.info("RUN serverPrincipal: "+KerberosUtil.getServicePrincipal("HTTP", serverName)); + gssCreds = gssManager.createCredential( + gssManager.createName( + KerberosUtil.getServicePrincipal("HTTP", serverName), + KerberosUtil.getOidInstance("NT_GSS_KRB5_PRINCIPAL")), + GSSCredential.INDEFINITE_LIFETIME, + new Oid[]{ + KerberosUtil.getOidInstance("GSS_SPNEGO_MECH_OID"), + KerberosUtil.getOidInstance("GSS_KRB5_MECH_OID")}, + GSSCredential.ACCEPT_ONLY); + gssContext = gssManager.createContext(gssCreds); + if (clientToken != null && clientToken.length > 0) { + LOG.info("Run ClientToken Object: "+clientToken.toString()); + LOG.info("Run ClientToken Length Object: "+clientToken.length); + } else { + LOG.info("Run ClientToken Null"); + } + + + byte[] serverToken = gssContext.acceptSecContext(clientToken, 0, clientToken.length); + + if (serverToken != null && serverToken.length > 0) { + LOG.info("Run ServerToken Object: "+serverToken.toString()); + LOG.info("Run ServerToken Length Object: "+serverToken.length); + String authenticate = base64.encodeToString(serverToken); + LOG.info("Run Authenticate Object: "+authenticate); + response.setHeader(KerberosAuthenticator.WWW_AUTHENTICATE, + KerberosAuthenticator.NEGOTIATE + " " + authenticate); + } else { + LOG.info("Run ServerToken Null"); + } + + if (!gssContext.isEstablished()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + LOG.info("SPNEGO in progress"); + } else { + String clientPrincipal = gssContext.getSrcName().toString(); + LOG.info("Run clientPrincipal Object: "+clientPrincipal); + + KerberosName kerberosName = new KerberosName(clientPrincipal); + LOG.info("Run kerberosName Object: "+kerberosName); + + String userName = kerberosName.getShortName(); + LOG.info("Run userName Object: "+userName); + + token = new AuthenticationToken(userName, clientPrincipal, getType()); + LOG.info("Run Token Object: "+token); + + response.setStatus(HttpServletResponse.SC_OK); + LOG.info("SPNEGO completed for principal [{}]", clientPrincipal); + } + } finally { + if (gssContext != null) { + gssContext.dispose(); + } + if (gssCreds != null) { + gssCreds.dispose(); + } + } + return token; + } + }); + } catch (PrivilegedActionException ex) { + ex.printStackTrace(); + + if (ex.getException() instanceof IOException) { + throw (IOException) ex.getException(); + } + else { + throw new AuthenticationException(ex.getException()); + } + } + } + return token; + } + + /** + * This method parses the User-Agent String and returns whether or not it + * refers to a browser. If its not a browser, then Kerberos authentication + * will be used; if it is a browser, alternateAuthenticate from the subclass + * will be used. + *

+ * A User-Agent String is considered to be a browser if it does not contain + * any of the values from alt-kerberos.non-browser.user-agents; the default + * behavior is to consider everything a browser unless it contains one of: + * "java", "curl", "wget", or "perl". Subclasses can optionally override + * this method to use different behavior. + * + * @param userAgent The User-Agent String, or null if there isn't one + * @return true if the User-Agent String refers to a browser, false if not + */ + protected boolean isBrowser(String userAgent) { + if (userAgent == null) { + return false; + } + userAgent = userAgent.toLowerCase(Locale.ENGLISH); + boolean isBrowser = true; + for (String nonBrowserUserAgent : nonBrowserUserAgents) { + if (userAgent.contains(nonBrowserUserAgent)) { + isBrowser = false; + break; + } + } + return isBrowser; + } + + +} diff --git a/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosDelegationAuthHandler.java b/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosDelegationAuthHandler.java new file mode 100644 index 0000000..8242a71 --- /dev/null +++ b/src/main/java/org/apache/hadoop/yarn/ats/AltATSLdapKerberosDelegationAuthHandler.java @@ -0,0 +1,37 @@ +package org.apache.hadoop.yarn.ats; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.security.authentication.server.AuthenticationHandler; +import org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler; +import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler; + + /** + * An {@link AuthenticationHandler} that implements Kerberos SPNEGO mechanism + * for HTTP and supports Delegation Token functionality. + *

+ * In addition to the {@link KerberosAuthenticationHandler} configuration + * properties, this handler supports: + *

+ */ + @InterfaceAudience.Private + @InterfaceStability.Evolving + public class AltATSLdapKerberosDelegationAuthHandler + extends DelegationTokenAuthenticationHandler { + + public AltATSLdapKerberosDelegationAuthHandler() { + super(new AltATSLdapKerberosAuthenticationHandler(AltATSLdapKerberosAuthenticationHandler.TYPE + + TYPE_POSTFIX)); + } +}