diff --git common/src/java/org/apache/hadoop/hive/conf/HiveConf.java common/src/java/org/apache/hadoop/hive/conf/HiveConf.java index eff4d30..f2e64eb 100644 --- common/src/java/org/apache/hadoop/hive/conf/HiveConf.java +++ common/src/java/org/apache/hadoop/hive/conf/HiveConf.java @@ -1891,6 +1891,26 @@ 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_GROUP_BASES("hive.server2.authentication.ldap.groupDNPattern", null, + "COLON-seperated 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_GROUP_FILTER("hive.server2.authentication.ldap.groupFilter", null, + "COLON-seperated list of LDAP Group names either ending with grou or just short names"), + HIVE_SERVER2_PLAIN_LDAP_USER_BASES("hive.server2.authentication.ldap.userDNPattern", null, + "COLON-seperated 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_USER_FILTER("hive.server2.authentication.ldap.userFilter", null, + "COLON-seperated list of LDAP usernames either ending with userBaseDN or just short names"), + HIVE_SERVER2_PLAIN_LDAP_ATN_QUERY("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 service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java index 4b95503..90178c2 100644 --- service/src/java/org/apache/hive/service/auth/AuthenticationProviderFactory.java +++ 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 service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java index 4e2ef90..9794c56 100644 --- service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java +++ service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java @@ -17,26 +17,91 @@ */ 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.Iterator; + 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.naming.ldap.LdapName; 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 String[] groupBases; + private static String[] userBases; + private String[] userFilter; + ArrayList filterGroups; + private 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_ATN_QUERY); + + if (customQuery == null) + { + String groupDNPatterns = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUP_BASES); + String groupFilterVal = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUP_FILTER); + String userDNPatterns = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USER_BASES); + String userFilterVal = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USER_FILTER); + + // 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) + { + groupBases = groupDNPatterns.split(":"); + } + else + { + groupBases = new String[] { "CN=%s," + baseDN }; + } + + if (groupFilterVal != null && groupFilterVal.trim().length() > 0) + { + filterGroups = new ArrayList(); + String[] groups = groupFilterVal.split(","); + for (int i = 0; i < groups.length; i++) + { + LOG.debug("Filtered group: " + groups[i]); + filterGroups.add(groups[i]); + } + } + + if (userDNPatterns != null && userDNPatterns.trim().length() > 0) + { + userBases = userDNPatterns.split(":"); + } + else + { + userBases = new String[] { "CN=%s," + baseDN }; + } + + if (userFilterVal != null && userFilterVal.trim().length() > 0) + { + userFilter = userFilterVal.split(","); + } + } } @Override @@ -58,27 +123,467 @@ 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); - try { + 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(); - } catch (NamingException e) { - throw new AuthenticationException("Error validating LDAP user", e); - } + ctx = new InitialDirContext(env); + + if (userFilter == null && filterGroups == 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) + { + ArrayList resultList = executeLDAPQuery(ctx, customQuery, baseDN); + if (resultList != null && resultList.size() > 0) + { + for (int i = 0; i < resultList.size(); i++) + { + String matchedDN = resultList.get(i); + 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.length > 0) + { + for (int i = 0; i < userBases.length; i++) + { + userDN = findUserDNByPattern(ctx, user); + if (userDN != null) + break; + else + LOG.debug("User not found in " + userBases[i]); + } + + LOG.info("Authenticating user " + user + " using user filter " + userFilter); + + boolean success = false; + for (int i = 0; i < userFilter.length; i++) + { + if (userFilter[i].equalsIgnoreCase(user)) + { + LOG.debug("User filter satisfied"); + success = true; + break; + } + } + + if (!success) + { + LOG.info("Authentication failed based on user membership"); + throw new AuthenticationException("Authentication failed: User not a member of listed groups"); + } + } + + if (filterGroups != null && filterGroups.size() > 0) + { + LOG.debug("Authenticating user " + user); + LOG.debug("Checking for group membership:"); + + // if only groupFilter is configured. + if (userDN == null) + { + userDN = findUserDNByName(ctx, baseDN, user); + } + + ArrayList userGroups = getGroupsForUser(ctx, userDN); + if (LOG.isDebugEnabled()) + { + LOG.debug("User member of :"); + prettyPrint(userGroups); + } + + for (int i = 0; i < userGroups.size(); i++) + { + String shortName = ((userGroups.get(i).split(","))[0].split("="))[1]; + String groupDN = userGroups.get(i).split(",", 2)[1]; + LOG.debug("Checking group:DN=" + userGroups.get(i) + ",shortName=" + shortName + ",groupDN=" + groupDN); + if (filterGroups.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("LDAP Authentication failed for user", e); + } + finally + { + try + { + if (ctx != null) + { + ctx.close(); + } + } + catch(Exception e) + { + e.printStackTrace(); + } + } + + return; } private boolean hasDomain(String userName) { return (ServiceUtils.indexOfDomainMatch(userName) > 0); } + + /** + * Util method that returns all the users present in the LDAP Directory. The search is based on + * objectClass attribute being either "user" or "person". This method is currently not used in the code. + * @param DirContext LDAPContext to execute the query within. + * @param ldapSearchBase RootDN/BaseDN of LDAP within which to perform this search. + * @return List contains DNs for all users that are readable by the bindDN for this context. + */ + public static ArrayList getUsers(DirContext ctx, String ldapSearchBase) throws NamingException + { + ArrayList userList = new ArrayList(); + String searchFilter = "(|(objectClass=user)(objectClass=person))"; + SearchControls searchControls = new SearchControls(); + String[] attrIDs = { DN_ATTR }; + + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(attrIDs); + + LOG.debug(searchFilter + ",base=" + ldapSearchBase); + NamingEnumeration results = ctx.search(ldapSearchBase, searchFilter, searchControls); + while(results.hasMoreElements()) + { + NamingEnumeration users = results.next().getAttributes().getAll(); + while (users.hasMore()) + { + Attribute attr = users.next(); + NamingEnumeration list = attr.getAll(); + while (list.hasMore()) + { + String user = (String)list.next(); + if (!userList.contains(user)) + userList.add(user); + } + } + } + return userList; + } + + private static void prettyPrint(ArrayList list) + { + Iterator iter = list.iterator(); + while(iter.hasNext()) + { + LOG.debug(" " + iter.next()); + } + } + + 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 DirContext Context 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); + + NamingEnumeration results = ctx.search(baseDN, searchFilter, searchControls); + SearchResult searchResult = null; + if(results.hasMoreElements()) + { + searchResult = (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 subsituted in the LDAP Query. + * @param DirContext Context 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 + { + 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 (int i = 0; i < groupBases.length; i++) + { + searchFilter = "(" + DN_ATTR + "=" + groupBases[i].replaceAll("%s", groupName) + ")"; + searchBase = groupBases[i].split(",",2)[1]; + results = ctx.search(searchBase, searchFilter, searchControls); + + if(results.hasMoreElements()) + { + searchResult = (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 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 DirContext Context 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); + + NamingEnumeration results = ctx.search(baseDN, searchFilter, searchControls); + SearchResult searchResult = null; + if(results.hasMoreElements()) + { + searchResult = (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 = (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 DirContext Context 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 + { + 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 (int i = 0; i < userBases.length; i++) + { + searchFilter = "(" + DN_ATTR + "=" + userBases[i].replaceAll("%s",userName) + ")"; + searchBase = userBases[i].split(",",2)[1]; + results = ctx.search(searchBase, searchFilter, searchControls); + + if(results.hasMoreElements()) + { + searchResult = (SearchResult) results.nextElement(); + //make sure there is not another entry with the same DN, there should be only 1 match + if(results.hasMoreElements()) + { + System.err.println("Matched multiple users for the user: " + userName); + return null; + } + return (String)searchResult.getAttributes().get(DN_ATTR).get(); + } + } + return null; + } + + /** + * 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 DirContext Context 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 ArrayList getGroupsForUser(DirContext ctx, String userDN) throws NamingException + { + ArrayList 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 DirContext LDAP Context to execute this query within. + * @param query User-defined LDAP Query string to be used to authenticate users. + * @return List of LDAP DNs returned from executing the LDAP Query. + */ + public static ArrayList executeLDAPQuery(DirContext ctx, String query, String rootDN) throws NamingException + { + SearchControls searchControls = new SearchControls(); + ArrayList 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 = (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; + } }