diff --git a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index eff4d30..a593db2 100644 --- a/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ b/common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -1891,6 +1891,28 @@ public void setSparkConfigUpdated(boolean isSparkConfigUpdated) { " order specified until a connection is successful."), HIVE_SERVER2_PLAIN_LDAP_BASEDN("hive.server2.authentication.ldap.baseDN", null, "LDAP base DN"), HIVE_SERVER2_PLAIN_LDAP_DOMAIN("hive.server2.authentication.ldap.Domain", null, ""), + HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN("hive.server2.authentication.ldap.groupDNPattern", null, + "COLON-separated list of patterns to use to find DNs for group entities in this directory \n" + + "use %s where the actual group name is to be substituted for.\n" + + "For example: CN=%s,CN=Groups,DC=subdomain,DC=domain,DC=com."), + HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER("hive.server2.authentication.ldap.groupFilter", null, + "COMMA-separated list of LDAP Group names (short name not full DNs) \n" + + " For example: HiveAdmins,HadoopAdmins,Administrators"), + HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN("hive.server2.authentication.ldap.userDNPattern", null, + "COLON-separated list of patterns to use to find DNs for users in this directory \n" + + "use %s where the actual group name is to be substituted for.\n" + + "For example: CN=%s,CN=Users,DC=subdomain,DC=domain,DC=com." + + "COLON-seperated list of Base DNs for User entities in the LDAP directory"), + HIVE_SERVER2_PLAIN_LDAP_USERFILTER("hive.server2.authentication.ldap.userFilter", null, + "COMMA-separated list of LDAP usernames (just short names, not full DNs) \n" + + "For example: hiveuser,impalauser,hiveadmin,hadoopadmin"), + HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY("hive.server2.authentication.ldap.customLDAPQuery", null, + "A full LDAP query that LDAP Atn provider uses to execute against LDAP Server \n" + + "If this query return a null resultset, the LDAP Provider fails the Authentication request \n" + + ", succeeds otherwise." + + "For example: (&(objectClass=group)(objectClass=top)(instanceType=4)(cn=Domain*)) \n" + + "(&(objectClass=person)(|(sAMAccountName=admin)(|(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com)" + + "(memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com))))"), HIVE_SERVER2_CUSTOM_AUTHENTICATION_CLASS("hive.server2.custom.authentication.class", null, "Custom authentication class. Used when property\n" + "'hive.server2.authentication' is set to 'CUSTOM'. Provided class\n" + diff --git a/service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java b/service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java index 4b95503..90178c2 100644 --- a/service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java +++ b/service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java @@ -24,6 +24,10 @@ */ public final class AuthenticationProviderFactory { + private static LdapAuthenticationProviderImpl ldapProvider; + private static PamAuthenticationProviderImpl pamProvider; + private static CustomAuthenticationProviderImpl customProvider; + public enum AuthMethods { LDAP("LDAP"), PAM("PAM"), @@ -57,11 +61,20 @@ private AuthenticationProviderFactory() { public static PasswdAuthenticationProvider getAuthenticationProvider(AuthMethods authMethod) throws AuthenticationException { if (authMethod == AuthMethods.LDAP) { - return new LdapAuthenticationProviderImpl(); + if (ldapProvider == null) + ldapProvider = new LdapAuthenticationProviderImpl(); + + return ldapProvider; } else if (authMethod == AuthMethods.PAM) { - return new PamAuthenticationProviderImpl(); + if (pamProvider == null) + pamProvider = new PamAuthenticationProviderImpl(); + + return pamProvider; } else if (authMethod == AuthMethods.CUSTOM) { - return new CustomAuthenticationProviderImpl(); + if (pamProvider == null) + customProvider = new CustomAuthenticationProviderImpl(); + + return customProvider; } else if (authMethod == AuthMethods.NONE) { return new AnonymousAuthenticationProviderImpl(); } else { diff --git a/service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java b/service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java index 4e2ef90..2f2cf3b 100644 --- a/service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java +++ b/service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java @@ -17,30 +17,109 @@ */ package org.apache.hive.service.auth; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hive.service.ServiceUtils; + +import java.util.ArrayList; import java.util.Hashtable; +import java.util.List; + import javax.naming.Context; import javax.naming.NamingException; +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +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.sasl.AuthenticationException; -import org.apache.hadoop.hive.conf.HiveConf; -import org.apache.hive.service.ServiceUtils; - public class LdapAuthenticationProviderImpl implements PasswdAuthenticationProvider { + private static final Log LOG = LogFactory.getLog(LdapAuthenticationProviderImpl.class); + private static final String DN_ATTR = "distinguishedName"; + private final String ldapURL; private final String baseDN; private final String ldapDomain; + private static List groupBases; + private static List userBases; + private static List userFilter; + private static List groupFilter; + private final String customQuery; LdapAuthenticationProviderImpl() { HiveConf conf = new HiveConf(); - ldapURL = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_URL); - baseDN = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN); - ldapDomain = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_DOMAIN); + ldapURL = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_URL); + baseDN = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN); + ldapDomain = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_DOMAIN); + customQuery = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY); + + if (customQuery == null) { + groupBases = new ArrayList(); + userBases = new ArrayList(); + String groupDNPatterns = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN); + String groupFilterVal = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER); + String userDNPatterns = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN); + String userFilterVal = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER); + + // parse COLON delimited root DNs for users/groups that may or may not be under BaseDN. + // Expect the root DNs be fully qualified including the baseDN + if (groupDNPatterns != null && groupDNPatterns.trim().length() > 0) { + String[] groupTokens = groupDNPatterns.split(":"); + for (int i = 0; i < groupTokens.length; i++) { + if (groupTokens[i].contains(",") && groupTokens[i].contains("=")) { + groupBases.add(groupTokens[i]); + } else { + LOG.info("Unexpected format for groupDNPattern..ignoring " + groupTokens[i]); + } + } + } else { + groupBases.add("CN=%s," + baseDN); + } + + if (groupFilterVal != null && groupFilterVal.trim().length() > 0) { + groupFilter = new ArrayList(); + String[] groups = groupFilterVal.split(","); + for (int i = 0; i < groups.length; i++) { + if (LOG.isDebugEnabled()) { + LOG.debug("Filtered group: " + groups[i]); + } + groupFilter.add(groups[i]); + } + } + + if (userDNPatterns != null && userDNPatterns.trim().length() > 0) { + String[] userTokens = userDNPatterns.split(":"); + for (int i = 0; i < userTokens.length; i++) { + if (userTokens[i].contains(",") && userTokens[i].contains("=")) { + userBases.add(userTokens[i]); + } else { + LOG.info("Unexpected format for userDNPattern..ignoring " + userTokens[i]); + } + } + } else { + userBases.add("CN=%s," + baseDN); + } + + if (userFilterVal != null && userFilterVal.trim().length() > 0) { + userFilter = new ArrayList(); + String[] users = userFilterVal.split(","); + for (int i = 0; i < users.length; i++) { + if (LOG.isDebugEnabled()) { + LOG.debug("Filtered user: " + users[i]); + } + userFilter.add(users[i]); + } + } + } } @Override - public void Authenticate(String user, String password) throws AuthenticationException { + public synchronized void Authenticate(String user, String password) throws AuthenticationException { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); @@ -58,27 +137,361 @@ public void Authenticate(String user, String password) throws AuthenticationExce " a null or blank password has been provided"); } - // setup the security principal - String bindDN; - if (baseDN == null) { - bindDN = user; - } else { - bindDN = "uid=" + user + "," + baseDN; - } + // user being authenticated becomes the bindDN and baseDN or userDN is used to search env.put(Context.SECURITY_AUTHENTICATION, "simple"); - env.put(Context.SECURITY_PRINCIPAL, bindDN); + env.put(Context.SECURITY_PRINCIPAL, user); env.put(Context.SECURITY_CREDENTIALS, password); + LOG.debug("Connecting using principal=" + user + " at url=" + ldapURL); + + DirContext ctx = null; + String userDN = null; try { // Create initial context - Context ctx = new InitialDirContext(env); - ctx.close(); + ctx = new InitialDirContext(env); + + if (userFilter == null && groupFilter == null && customQuery == null) { + userDN = findUserDNByPattern(ctx, user); + + if (userDN == null) { + userDN = findUserDNByName(ctx, baseDN, user); + } + + // This should not be null because we were allowed to bind with this username + // safe check in case we were able to bind anonymously. + if (userDN == null) { + throw new AuthenticationException("Authentication failed: User search failed"); + } + return; + } + + if (customQuery != null) { + List resultList = executeLDAPQuery(ctx, customQuery, baseDN); + if (resultList != null) { + for (String matchedDN : resultList) { + if (matchedDN.split(",",2)[0].split("=",2)[1].equalsIgnoreCase(user)) { + LOG.info("Authentication succeeded based on result set from LDAP query"); + return; + } + } + } + throw new AuthenticationException("Authentication failed: LDAP query " + + "from property returned no data"); + } + + // This section checks if the user satisfies the specified user filter. + if (userFilter != null && userFilter.size() > 0) { + LOG.info("Authenticating user " + user + " using user filter"); + + boolean success = false; + for (String filteredUser : userFilter) { + if (filteredUser.equalsIgnoreCase(user)) { + LOG.debug("User filter partially satisfied"); + success = true; + break; + } + } + + if (!success) { + LOG.info("Authentication failed based on user membership"); + throw new AuthenticationException("Authentication failed: User not a member " + + "of specified list"); + } + + userDN = findUserDNByPattern(ctx, user); + if (userDN != null) { + LOG.info("User filter entirely satisfied"); + } else { + LOG.info("User " + user + " could not be found in the configured UserBaseDN," + + "authentication failed"); + throw new AuthenticationException("Authentication failed: UserDN could not be " + + "found in specified User base(s)"); + } + } + + if (groupFilter != null && groupFilter.size() > 0) { + LOG.debug("Authenticating user " + user + " using group membership:"); + + // if only groupFilter is configured. + if (userDN == null) { + userDN = findUserDNByName(ctx, baseDN, user); + } + + List userGroups = getGroupsForUser(ctx, userDN); + if (LOG.isDebugEnabled()) { + LOG.debug("User member of :"); + prettyPrint(userGroups); + } + + if (userGroups != null) { + for (String elem : userGroups) { + String shortName = ((elem.split(","))[0].split("="))[1]; + String groupDN = elem.split(",", 2)[1]; + LOG.debug("Checking group:DN=" + elem + ",shortName=" + shortName + + ",groupDN=" + groupDN); + if (groupFilter.contains(shortName)) { + LOG.info("Authentication succeeded based on group membership"); + return; + } + } + } + + throw new AuthenticationException("Authentication failed: User not a member of " + + "listed groups"); + } + + LOG.info("Simple password authentication succeeded"); + } catch (NamingException e) { - throw new AuthenticationException("Error validating LDAP user", e); + throw new AuthenticationException("LDAP Authentication failed for user", e); + } finally { + try { + if (ctx != null) { + ctx.close(); + } + } catch(Exception e) { + e.printStackTrace(); + } } } private boolean hasDomain(String userName) { return (ServiceUtils.indexOfDomainMatch(userName) > 0); } + + private static void prettyPrint(List list) { + for (String elem : list) { + LOG.debug(" " + elem); + } + } + + private static void prettyPrint(Attributes attrs) { + NamingEnumeration set = attrs.getAll(); + try { + NamingEnumeration list = null; + while (set.hasMore()) { + Attribute attr = set.next(); + list = attr.getAll(); + String attrVals = ""; + while (list.hasMore()) { + attrVals += list.next() + "+"; + } + LOG.debug(attr.getID() + ":::" + attrVals); + } + } catch (Exception e) { + System.out.println("Error occurred when reading ldap data:" + e.getMessage()); + } + } + + /** + * This helper method attempts to find a DN given a unique groupname. + * Various LDAP implementations have different keys/properties that store this unique ID. + * So the first attempt is to find an entity with objectClass=group && CN=groupName + * @param ctx DirContext for the LDAP Connection. + * @param baseDN BaseDN for this LDAP directory where the search is to be performed. + * @param groupName A unique groupname that is to be located in the LDAP. + * @return LDAP DN if the group is found in LDAP, null otherwise. + */ + public static String findGroupDNByName(DirContext ctx, String baseDN, String groupName) + throws NamingException { + String searchFilter = "(&(objectClass=group)(CN=" + groupName + "))"; + SearchControls searchControls = new SearchControls(); + String[] returnAttributes = { DN_ATTR }; + + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(returnAttributes); + searchControls.setCountLimit(2); //limit to 2 results in case of misconfigured rootDN + + NamingEnumeration results = ctx.search(baseDN, searchFilter, searchControls); + SearchResult searchResult = null; + if(results.hasMoreElements()) { + searchResult = results.nextElement(); + //make sure there is not another item available, there should be only 1 match + if(results.hasMoreElements()) { + LOG.info("Matched multiple groups for the group: " + groupName); + return null; + } + return (String)searchResult.getAttributes().get(DN_ATTR).get(); + } + return null; + } + + /** + * This helper method attempts to find an LDAP group entity given a unique name using a user-defined pattern for GROUPBASE. + * The list of group bases is defined by the user via property "hive.server2.authentication.ldap.groupDNPattern" + * in the hive-site.xml. Users can use %s where the actual groupname is to be substituted in the LDAP Query. + * @param ctx DirContext for the LDAP Connection. + * @param groupName A unique groupname that is to be located in the LDAP. + * @return LDAP DN of given group if found in the directory, null otherwise. + */ + public static String findGroupDNByPattern(DirContext ctx, String groupName) + throws NamingException { + return findDNByPattern(ctx, groupName, groupBases); + } + + public static String findDNByPattern(DirContext ctx, String name, List nodes) + throws NamingException { + String searchFilter; + String searchBase; + SearchResult searchResult = null; + NamingEnumeration results; + + String[] returnAttributes = { DN_ATTR }; + SearchControls searchControls = new SearchControls(); + + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(returnAttributes); + + for (String node : nodes) { + searchFilter = "(" + DN_ATTR + "=" + node.replaceAll("%s", name) + ")"; + searchBase = node.split(",",2)[1]; + results = ctx.search(searchBase, searchFilter, searchControls); + + if(results.hasMoreElements()) { + searchResult = results.nextElement(); + //make sure there is not another item available, there should be only 1 match + if(results.hasMoreElements()) { + LOG.info("Matched multiple entities for the name: " + name); + return null; + } + return (String)searchResult.getAttributes().get(DN_ATTR).get(); + } + } + return null; + } + + /** + * This helper method attempts to find a DN given a unique username. + * Various LDAP implementations have different keys/properties that store this unique userID. + * Active Directory has a "sAMAccoutName" that appears reliable. + * openLDAP uses "uid" + * So the first attempt is to find an entity with objectClass=person||user where uid||sAMAccountName matches + * the given username. + * The second attempt is to use CN attribute for wild card matching and then match the username in the DN. + * @param ctx DirContext for the LDAP Connection. + * @param baseDN BaseDN for this LDAP directory where the search is to be performed. + * @param userName A unique userid that is to be located in the LDAP. + * @return LDAP DN if the user is found in LDAP, null otherwise. + */ + public static String findUserDNByName(DirContext ctx, String baseDN, String userName) + throws NamingException { + String baseFilter = "(&(|(objectClass=person)(objectClass=user))"; + String suffix[] = new String[] { + "(|(uid=" + userName + ")(sAMAccountName=" + userName + ")))", + "(|(cn=*" + userName + "*)))" + }; + String searchFilter = baseFilter + suffix[0]; + SearchControls searchControls = new SearchControls(); + String[] returnAttributes = { DN_ATTR }; + + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(returnAttributes); + searchControls.setCountLimit(2); // limit the result set to 2 in case of a misconfigured rootDN + + NamingEnumeration results = ctx.search(baseDN, searchFilter, searchControls); + SearchResult searchResult = null; + if(results.hasMoreElements()) { + searchResult = results.nextElement(); + //make sure there is not another item available, there should be only 1 match + if(results.hasMoreElements()) { + LOG.info("Matched multiple users for the user: " + userName); + return null; + } + return (String)searchResult.getAttributes().get(DN_ATTR).get(); + } else { + searchFilter = baseFilter + suffix[1]; + results = ctx.search(baseDN, searchFilter, searchControls); + + String matchedDN; + while(results.hasMoreElements()) { + searchResult = results.nextElement(); + matchedDN = (String)searchResult.getAttributes().get(DN_ATTR).get(); + + if(matchedDN.split(",",2)[0].split("=",2)[1].equals(userName)) { + return matchedDN; + } + } + } + return null; + } + + /** + * This helper method attempts to find a UserDN given a unique username from a user-defined pattern for USERBASE. + * The list of user bases is defined by the user via property "hive.server2.authentication.ldap.userDNPattern" + * in the hive-site.xml. Users can use %s where the actual username is to be subsituted in the LDAP Query. + * @param ctx DirContext for the LDAP Connection. + * @param userName A unique userid that is to be located in the LDAP. + * @return LDAP DN of given user if found in the directory, null otherwise. + */ + public static String findUserDNByPattern(DirContext ctx, String userName) + throws NamingException { + return findDNByPattern(ctx, userName, userBases); + } + + /** + * This helper method finds all the groups a given user belongs to. + * This method relies on the "memberOf" attribute being set on the user that references the group the group. + * The returned list ONLY includes direct groups the user belongs to. Parent groups of these direct groups + * are NOT included. + * @param ctx DirContext for the LDAP Connection. + * @param userName A unique userid that is to be located in the LDAP. + * @return List of Group DNs the user belongs to, emptylist otherwise. + */ + public static List getGroupsForUser(DirContext ctx, String userDN) throws NamingException { + List groupList = new ArrayList(); + String searchFilter = "(" + DN_ATTR + "=" + userDN + ")"; + SearchControls searchControls = new SearchControls(); + + LOG.debug("getGroupsForUser:searchFilter=" + searchFilter); + String[] attrIDs = { "memberOf" }; + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(attrIDs); + + // treat everything after the first COMMA as a baseDN for the search to find this user + NamingEnumeration results = ctx.search(userDN.split(",",2)[1], searchFilter, searchControls); + while(results.hasMoreElements()) { + NamingEnumeration groups = results.next().getAttributes().getAll(); + while (groups.hasMore()) { + Attribute attr = groups.next(); + NamingEnumeration list = attr.getAll(); + while (list.hasMore()) { + groupList.add((String)list.next()); + } + } + } + return groupList; + } + + /** + * This method helps execute a LDAP query defined by the user via "hive.server2.authentication.ldap.customLDAPQuery" + * A full LDAP query that LDAP Atn provider uses to execute against LDAP Server. + * If this query return a null resultset, the LDAP Provider fails the authentication request. + * If the LDAP query returns a list of DNs, a check is performed to confirm one of the entries is for the user being + * authenticated. + * For example: (&(objectClass=group)(objectClass=top)(instanceType=4)(cn=Domain*)) + * (&(objectClass=person)(|(sAMAccountName=admin)(|(memberOf=CN=Domain Admins,CN=Users,DC=domain,DC=com) + * (memberOf=CN=Administrators,CN=Builtin,DC=domain,DC=com)))) + * @param ctx DirContext to execute this query within. + * @param query User-defined LDAP Query string to be used to authenticate users. + * @param rootDN BaseDN at which to execute the LDAP query, typically rootDN for the LDAP. + * @return List of LDAP DNs returned from executing the LDAP Query. + */ + public static List executeLDAPQuery(DirContext ctx, String query, String rootDN) throws NamingException { + SearchControls searchControls = new SearchControls(); + List list = new ArrayList(); + String[] returnAttributes = { DN_ATTR }; + + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(returnAttributes); + + LOG.info("Using a user specified LDAP query for adjudication:" + query + ",baseDN=" + rootDN); + NamingEnumeration results = ctx.search(rootDN, query, searchControls); + SearchResult searchResult = null; + while(results.hasMoreElements()) { + searchResult = results.nextElement(); + list.add((String)searchResult.getAttributes().get(DN_ATTR).get()); + LOG.debug("LDAPAtn:executeLDAPQuery()::Return set size " + list.get(list.size() - 1)); + } + return list; + } }