diff --git service/pom.xml service/pom.xml index ecea719..0595d7e 100644 --- service/pom.xml +++ service/pom.xml @@ -162,6 +162,12 @@ ${junit.version} test + + + org.mockito + mockito-all + test + org.apache.directory.client.ldap diff --git service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java index efd5393..2a818c9 100644 --- service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java +++ service/src/java/org/apache/hive/service/auth/LdapAuthenticationProviderImpl.java @@ -17,633 +17,102 @@ */ package org.apache.hive.service.auth; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; -import java.util.ListIterator; - -import javax.naming.Context; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -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 com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import java.util.Iterator; +import java.util.List; +import org.apache.commons.lang.StringUtils; import org.apache.hadoop.hive.conf.HiveConf; import org.apache.hive.service.ServiceUtils; +import org.apache.hive.service.auth.ldap.ChainFilterFactory; +import org.apache.hive.service.auth.ldap.CustomQueryFilterFactory; +import org.apache.hive.service.auth.ldap.LdapSearchFactory; +import org.apache.hive.service.auth.ldap.Filter; +import org.apache.hive.service.auth.ldap.DirSearch; +import org.apache.hive.service.auth.ldap.DirSearchFactory; +import org.apache.hive.service.auth.ldap.FilterFactory; +import org.apache.hive.service.auth.ldap.GroupFilterFactory; +import org.apache.hive.service.auth.ldap.LdapUtils; +import org.apache.hive.service.auth.ldap.UserFilterFactory; +import org.apache.hive.service.auth.ldap.UserSearchFilterFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LdapAuthenticationProviderImpl implements PasswdAuthenticationProvider { - private static final Logger LOG = LoggerFactory.getLogger(LdapAuthenticationProviderImpl.class); + private static final Logger LOG = LoggerFactory.getLogger(LdapAuthenticationProviderImpl.class); - private String ldapURL; - private String baseDN; - private String ldapDomain; - private static List groupBases; - private static List userBases; - private static List userFilter; - private static List groupFilter; - private String customQuery; - private static String guid_attr; - private static String groupMembership_attr; - private static String groupClass_attr; - - LdapAuthenticationProviderImpl(HiveConf conf) { - init(conf); - } + private static final List FILTER_FACTORIES = ImmutableList.of( + new CustomQueryFilterFactory(), + new ChainFilterFactory(new UserSearchFilterFactory(), new UserFilterFactory(), + new GroupFilterFactory()) + ); - protected void init(HiveConf conf) { - 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); - guid_attr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY); - groupBases = new ArrayList(); - userBases = new ArrayList(); - userFilter = new ArrayList(); - groupFilter = new ArrayList(); + private final HiveConf conf; + private final Filter filter; + private final DirSearchFactory searchFactory; - 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); - groupMembership_attr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPMEMBERSHIP_KEY); - groupClass_attr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPCLASS_KEY); - - // 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.warn("Unexpected format for " + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN - + "..ignoring " + groupTokens[i]); - } - } - } else if (baseDN != null) { - groupBases.add(guid_attr + "=%s," + baseDN); - } - - if (groupFilterVal != null && groupFilterVal.trim().length() > 0) { - 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.warn("Unexpected format for " + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN - + "..ignoring " + userTokens[i]); - } - } - } else if (baseDN != null) { - userBases.add(guid_attr + "=%s," + baseDN); - } + public LdapAuthenticationProviderImpl(HiveConf conf) { + this(conf, new LdapSearchFactory()); + } - if (userFilterVal != null && userFilterVal.trim().length() > 0) { - 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]); - } - } + @VisibleForTesting + LdapAuthenticationProviderImpl(HiveConf conf, DirSearchFactory searchFactory) { + this.conf = conf; + this.searchFactory = searchFactory; + filter = resolveFilter(conf); } @Override public void Authenticate(String user, String password) throws AuthenticationException { - - Hashtable env = new Hashtable(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - env.put(Context.PROVIDER_URL, ldapURL); - - // If the domain is available in the config, then append it unless domain is - // already part of the username. LDAP providers like Active Directory use a - // fully qualified user name like foo@bar.com. - if (!hasDomain(user) && ldapDomain != null) { - user = user + "@" + ldapDomain; - } - - if (password == null || password.isEmpty() || password.getBytes()[0] == 0) { - throw new AuthenticationException("Error validating LDAP user:" + - " a null or blank password has been provided"); - } - - env.put(Context.SECURITY_AUTHENTICATION, "simple"); - env.put(Context.SECURITY_CREDENTIALS, password); - - // setup the security principal - String bindDN = null; - DirContext ctx = null; - String userDN = null; - String userName = null; - Exception ex = null; - - if (!isDN(user) && !hasDomain(user) && userBases.size() > 0) { - ListIterator listIter = userBases.listIterator(); - while (listIter.hasNext()) { - try { - bindDN = listIter.next().replaceAll("%s", user); - env.put(Context.SECURITY_PRINCIPAL, bindDN); - LOG.debug("Connecting using DN " + bindDN + " at url " + ldapURL); - ctx = new InitialDirContext(env); - break; - } catch (NamingException e) { - ex = e; - } - } - } else { - env.put(Context.SECURITY_PRINCIPAL, user); - LOG.debug("Connecting using principal " + user + " at url " + ldapURL); - try { - ctx = new InitialDirContext(env); - } catch (NamingException e) { - ex = e; - } - } - - if (ctx == null) { - LOG.debug("Could not connect to the LDAP Server:Authentication failed for " + user); - throw new AuthenticationException("LDAP Authentication failed for user", ex); - } - - LOG.debug("Connected using principal=" + user + " at url=" + ldapURL); + DirSearch search = null; try { - if (isDN(user) || hasDomain(user)) { - userName = extractName(user); - } else { - userName = user; - } - - // if a custom LDAP query is specified, it takes precedence over other configuration properties. - // if the user being authenticated is part of the resultset from the custom query, it succeeds. - if (customQuery != null) { - List resultList = executeLDAPQuery(ctx, customQuery, baseDN); - if (resultList != null) { - for (String matchedDN : resultList) { - LOG.info(""); - if (matchedDN.split(",",2)[0].split("=",2)[1].equalsIgnoreCase(user) || - matchedDN.equalsIgnoreCase(user)) { - LOG.info("Authentication succeeded based on result set from LDAP query"); - return; - } - } - } - LOG.info("Authentication failed based on result set from custom LDAP query"); - throw new AuthenticationException("Authentication failed: LDAP query " + - "from property returned no data"); - } else if (userBases.size() > 0) { - if (isDN(user)) { - userDN = findUserDNByDN(ctx, user); - } else { - if (userDN == null) { - userDN = findUserDNByPattern(ctx, userName); - } - - if (userDN == null) { - userDN = findUserDNByName(ctx, userName); - } - } - - // 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"); - } - - // This section checks if the user satisfies the specified user filter. - if (userFilter.size() > 0) { - LOG.info("Authenticating user " + user + " using user filter"); - - if (userDN != null) { - LOG.info("User filter partially satisfied"); - } - - boolean success = false; - for (String filteredUser : userFilter) { - if (filteredUser.equalsIgnoreCase(userName)) { - LOG.debug("User filter entirely 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"); - } - } - - // This section checks if the user satisfies the specified user filter. - if (groupFilter.size() > 0) { - LOG.debug("Authenticating user " + user + " using group membership"); - 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]; - if (groupFilter.contains(shortName)) { - LOG.info("Authentication succeeded based on group membership"); - return; - } - } - } - - LOG.debug("Authentication failed: User is not a member of configured groups"); - throw new AuthenticationException("Authentication failed: User not a member of " + - "listed groups"); - } - LOG.info("Authentication succeeded using ldap user search"); - return; - } - // Ideally we should not be here. Indicates partially configured LDAP Service. - // We allow it for now for backward compatibility. - LOG.info("Simple password authentication succeeded"); - } catch (NamingException e) { - throw new AuthenticationException("LDAP Authentication failed for user", e); + search = createDirSearch(user, password); + applyFilter(search, user); } finally { - try { - if (ctx != null) { - ctx.close(); - } - } catch(Exception e) { - LOG.warn("Exception when closing LDAP context:" + e.getMessage()); - } - } - } - - 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()); + ServiceUtils.cleanup(LOG, search); } } - /** - * 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=" + groupClass_attr + ")(" + guid_attr + "=" + groupName + "))"; - List results = null; - - results = findDNByName(ctx, baseDN, searchFilter, 2); - - if (results == null) { - return null; - } else if (results.size() > 1) { - //make sure there is not another item available, there should be only 1 match - LOG.info("Matched multiple groups for the group: " + groupName + ",returning null"); - return null; - } - return results.get(0); - } - - /** - * 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 = new String[0]; // empty set - SearchControls searchControls = new SearchControls(); - - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - searchControls.setReturningAttributes(returnAttributes); - - for (String node : nodes) { - searchFilter = "(" + (node.substring(0,node.indexOf(","))).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.warn("Matched multiple entities for the name: " + name); - return null; - } - return searchResult.getNameInNamespace(); - } - } - 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 "sAMAccountName" 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 userName) - throws NamingException { - if (userBases.size() == 0) { - return null; - } - - String baseFilter = "(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))"; - String suffix[] = new String[] { - "(|(uid=" + userName + ")(sAMAccountName=" + userName + ")))", - "(|(cn=*" + userName + "*)))" - }; - - String searchFilter = null; - List results = null; - ListIterator listIter = userBases.listIterator(); - - for (int i = 0; i < suffix.length; i++) { - searchFilter = baseFilter + suffix[i]; - - while (listIter.hasNext()) { - results = findDNByName(ctx, listIter.next().split(",",2)[1], searchFilter, 2); - - if(results == null) { - continue; - } - - if(results != null && results.size() > 1) { - //make sure there is not another item available, there should be only 1 match - LOG.info("Matched multiple users for the user: " + userName + ",returning null"); - return null; - } - return results.get(0); - } - } - return null; - } - - /** - * This helper method attempts to find a username given a DN. - * Various LDAP implementations have different keys/properties that store this unique userID. - * Active Directory has a "sAMAccountName" 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 findUserDNByDN(DirContext ctx, String userDN) - throws NamingException { - if (!isDN(userDN)) { - return null; - } - - String baseDN = extractBaseDN(userDN); - List results = null; - // we are using the first part of the userDN in the search criteria. - // We know the DN is legal as we are able to bind with it, this is to confirm that its a user. - String searchFilter = "(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))(" - + userDN.substring(0,userDN.indexOf(",")) + "))"; - - results = findDNByName(ctx, baseDN, searchFilter, 2); - - if (results == null) { - return null; + private DirSearch createDirSearch(String user, String password) throws AuthenticationException { + if (StringUtils.isBlank(user)) { + throw new AuthenticationException("Error validating LDAP user:" + + " a null or blank user name has been provided"); } - - if(results.size() > 1) { - //make sure there is not another item available, there should be only 1 match - LOG.info("Matched multiple users for the user: " + userDN + ",returning null"); - return null; + if (StringUtils.isBlank(password) || password.getBytes()[0] == 0) { + throw new AuthenticationException("Error validating LDAP user:" + + " a null or blank password has been provided"); } - return results.get(0); - } - - public static List findDNByName(DirContext ctx, String baseDN, - String searchString, int limit) throws NamingException { - SearchResult searchResult = null; - List retValues = null; - String matchedDN = null; - SearchControls searchControls = new SearchControls(); - String[] returnAttributes = new String[0]; //empty set - - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - searchControls.setReturningAttributes(returnAttributes); - if (limit > 0) { - searchControls.setCountLimit(limit); // limit the result set to limit the size of resultset - } - - NamingEnumeration results = ctx.search(baseDN, searchString, searchControls); - while(results.hasMoreElements()) { - searchResult = results.nextElement(); - matchedDN = searchResult.getNameInNamespace(); - - if (retValues == null) { - retValues = new ArrayList(); - } - retValues.add(matchedDN); - } - return retValues; - } - - /** - * 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 attribute,configurable via HIVE_SERVER2_PLAIN_LDAP_GROUPMEMBERSHIP_KEY, - * being set on the user entry that references 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 userDN A unique userDN 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 user = extractName(userDN); - String searchFilter = "(&(objectClass=" + groupClass_attr + ")(|(" + - groupMembership_attr + "=" + userDN + ")(" + - groupMembership_attr + "=" + user + ")))"; - SearchControls searchControls = new SearchControls(); - NamingEnumeration results = null; - SearchResult result = null; - String groupBase = null; - - LOG.debug("getGroupsForUser:searchFilter=" + searchFilter); - String[] attrIDs = new String[0]; - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - searchControls.setReturningAttributes(attrIDs); - - ListIterator listIter = groupBases.listIterator(); - while (listIter.hasNext()) { + List principals = LdapUtils.createCandidatePrincipals(conf, user); + for (Iterator iterator = principals.iterator(); iterator.hasNext();) { + String principal = iterator.next(); try { - groupBase = listIter.next().split(",", 2)[1]; - LOG.debug("Searching for groups under " + groupBase); - results = ctx.search(groupBase, searchFilter, searchControls); - - while(results.hasMoreElements()) { - result = results.nextElement(); - LOG.debug("Found Group:" + result.getNameInNamespace()); - groupList.add(result.getNameInNamespace()); + return searchFactory.getInstance(conf, principal, password); + } catch (AuthenticationException ex) { + if (!iterator.hasNext()) { + throw ex; } - } catch (NamingException e) { - LOG.warn("Exception searching for user groups", e); } } - - return groupList; + throw new AuthenticationException( + String.format("No candidate principals for %s was found.", user)); } - /** - * This method helps execute a LDAP query defined by the user via property - * "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 { - if (rootDN == null) { - return null; - } - - SearchControls searchControls = new SearchControls(); - List list = new ArrayList(); - String[] returnAttributes; - if (groupMembership_attr != null) { - // retrieve the attributes that are meant to desginate user DNs - returnAttributes = new String[] { groupMembership_attr }; - } else { - returnAttributes = new String[0]; //empty set - } - - 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(); - if (groupMembership_attr != null) { - Attribute userAttribute = searchResult.getAttributes().get(groupMembership_attr); - if (userAttribute != null) { - list.add((String)userAttribute.get()); - continue; - } + private static Filter resolveFilter(HiveConf conf) { + for (FilterFactory filterProvider : FILTER_FACTORIES) { + Filter filter = filterProvider.getInstance(conf); + if (filter != null) { + return filter; } - - list.add(searchResult.getNameInNamespace()); - LOG.debug("LDAPAtn:executeLDAPQuery()::Return set size " + list.get(list.size() - 1)); } - return list; - } - - public static boolean isDN(String name) { - return (name.indexOf("=") > -1); - } - - public static String extractName(String dn) { - int domainIdx = ServiceUtils.indexOfDomainMatch(dn); - if (domainIdx > 0) { - return dn.substring(0, domainIdx); - } - - if (dn.indexOf("=") > -1) { - return dn.substring(dn.indexOf("=") + 1, dn.indexOf(",")); - } - return dn; + return null; } - public static String extractBaseDN(String dn) { - if (dn.indexOf(",") > -1) { - return dn.substring(dn.indexOf(",") + 1); + private void applyFilter(DirSearch client, String user) throws AuthenticationException { + if (filter != null) { + filter.apply(client, user); } - return null; } } diff --git service/src/java/org/apache/hive/service/auth/ldap/ChainFilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/ChainFilterFactory.java new file mode 100644 index 0000000..796b135 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/ChainFilterFactory.java @@ -0,0 +1,78 @@ +/** + * 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.hive.service.auth.ldap; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; + +/** + * A factory that produces a {@link Filter} that is implemented as a chain of other filters. + * The chain of filters are created as a result of + * {@link #getInstance(org.apache.hadoop.hive.conf.HiveConf) } + * method call. The resulting object filters out all users that don't pass all + * chained filters. The filters will be applied in the order they are mentioned in the factory + * constructor. + */ +public class ChainFilterFactory implements FilterFactory { + + private final List chainedFactories; + + /** + * Constructs a factory for a chain of filters. + * + * @param factories The array of factories that will be used to construct a chain of filters. + */ + public ChainFilterFactory(FilterFactory... factories) { + this.chainedFactories = ImmutableList.copyOf(factories); + } + + /** + * {@inheritDoc} + */ + @Override + public Filter getInstance(HiveConf conf) { + List filters = new ArrayList<>(); + for (FilterFactory factory : chainedFactories) { + Filter filter = factory.getInstance(conf); + if (filter != null) { + filters.add(filter); + } + } + + return filters.isEmpty() ? null : new ChainFilter(ImmutableList.copyOf(filters)); + } + + private static final class ChainFilter implements Filter { + + private final List chainedFilters; + + public ChainFilter(List chainedFilters) { + this.chainedFilters = chainedFilters; + } + + @Override + public void apply(DirSearch client, String user) throws AuthenticationException { + for (Filter filter : chainedFilters) { + filter.apply(client, user); + } + } + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/CustomQueryFilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/CustomQueryFilterFactory.java new file mode 100644 index 0000000..4dfcba6 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/CustomQueryFilterFactory.java @@ -0,0 +1,84 @@ +/** + * 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.hive.service.auth.ldap; + +import com.google.common.base.Strings; +import java.util.List; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for a {@link Filter} based on a custom query. + *
+ * The produced filter object filters out all users that are not found in the search result + * of the query provided in Hive configuration. + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY + */ +public class CustomQueryFilterFactory implements FilterFactory { + + /** + * {@inheritDoc} + */ + @Override + public Filter getInstance(HiveConf conf) { + String customQuery = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY); + + if (Strings.isNullOrEmpty(customQuery)) { + return null; + } + + return new CustomQueryFilter(customQuery); + } + + private static final class CustomQueryFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(CustomQueryFilter.class); + + private final String query; + + public CustomQueryFilter(String query) { + this.query = query; + } + + @Override + public void apply(DirSearch client, String user) throws AuthenticationException { + List resultList; + try { + resultList = client.executeCustomQuery(query); + } catch (NamingException e) { + throw new AuthenticationException("LDAP Authentication failed for user", e); + } + if (resultList != null) { + for (String matchedDn : resultList) { + String shortUserName = LdapUtils.getShortName(matchedDn); + LOG.info(""); + if (shortUserName.equalsIgnoreCase(user) || matchedDn.equalsIgnoreCase(user)) { + LOG.info("Authentication succeeded based on result set from LDAP query"); + return; + } + } + } + LOG.info("Authentication failed based on result set from custom LDAP query"); + throw new AuthenticationException("Authentication failed: LDAP query " + + "from property returned no data"); + } + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/DirSearch.java service/src/java/org/apache/hive/service/auth/ldap/DirSearch.java new file mode 100644 index 0000000..33b6088 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/DirSearch.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.hive.service.auth.ldap; + +import java.io.Closeable; +import java.util.List; +import javax.naming.NamingException; + +/** + * The object used for executing queries on the Directory Service. + */ +public interface DirSearch extends Closeable { + + /** + * Finds user's distinguished name. + * @param user username + * @return DN for the specified username + * @throws NamingException + */ + String findUserDn(String user) throws NamingException; + + /** + * Finds groups that contain the specified user. + * @param userDn user's distinguished name + * @return list of groups + * @throws NamingException + */ + List findGroupsForUser(String userDn) throws NamingException; + + /** + * Executes an arbitrary query. + * @param query any query + * @return list of names in the namespace + * @throws NamingException + */ + List executeCustomQuery(String query) throws NamingException; +} diff --git service/src/java/org/apache/hive/service/auth/ldap/DirSearchFactory.java service/src/java/org/apache/hive/service/auth/ldap/DirSearchFactory.java new file mode 100644 index 0000000..b5230db --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/DirSearchFactory.java @@ -0,0 +1,37 @@ +/** + * 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.hive.service.auth.ldap; + +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; + +/** + * A factory for {@code DirSearch}. + */ +public interface DirSearchFactory { + + /** + * Returns an instance of {@code DirSearch}. + * @param conf Hive configuration + * @param user username + * @param password user password + * @return instance of {@code DirSearch} + * @throws AuthenticationException + */ + DirSearch getInstance(HiveConf conf, String user, String password) throws AuthenticationException; +} diff --git service/src/java/org/apache/hive/service/auth/ldap/Filter.java service/src/java/org/apache/hive/service/auth/ldap/Filter.java new file mode 100644 index 0000000..fa72ced --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/Filter.java @@ -0,0 +1,36 @@ +/** + * 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.hive.service.auth.ldap; + +import javax.security.sasl.AuthenticationException; + +/** + * The object that filters LDAP users. + *
+ * The assumption is that this user was already authenticated by a previous bind operation. + */ +public interface Filter { + + /** + * Applies this filter to the authenticated user. + * @param client LDAP client that will be used for execution of LDAP queries. + * @param user username + * @throws AuthenticationException + */ + void apply(DirSearch client, String user) throws AuthenticationException; +} diff --git service/src/java/org/apache/hive/service/auth/ldap/FilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/FilterFactory.java new file mode 100644 index 0000000..c9db783 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/FilterFactory.java @@ -0,0 +1,33 @@ +/** + * 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.hive.service.auth.ldap; + +import org.apache.hadoop.hive.conf.HiveConf; + +/** + * Factory for the filter. + */ +public interface FilterFactory { + + /** + * Returns an instance of the corresponding filter. + * @param conf Hive properties used to configure the filter. + * @return the filter or {@code null} if this filter doesn't support provided set of properties + */ + Filter getInstance(HiveConf conf); +} diff --git service/src/java/org/apache/hive/service/auth/ldap/GroupFilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/GroupFilterFactory.java new file mode 100644 index 0000000..d433bc3 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/GroupFilterFactory.java @@ -0,0 +1,90 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for a {@link Filter} based on a list of allowed groups. + *
+ * The produced filter object filters out all users that are not members of at least one of + * the groups provided in Hive configuration. + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER + */ +public final class GroupFilterFactory implements FilterFactory { + + /** + * {@inheritDoc} + */ + @Override + public Filter getInstance(HiveConf conf) { + Collection groupFilter = conf.getStringCollection( + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER.varname); + + if (groupFilter.isEmpty()) { + return null; + } + + return new GroupFilter(groupFilter); + } + + private static final class GroupFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(GroupFilter.class); + + private final Set groupFilter = new HashSet<>(); + + GroupFilter(Collection groupFilter) { + this.groupFilter.addAll(groupFilter); + } + + @Override + public void apply(DirSearch ldap, String user) throws AuthenticationException { + LOG.info("Authenticating user '{}' using group membership", user); + + List memberOf = null; + + try { + String userDn = ldap.findUserDn(user); + memberOf = ldap.findGroupsForUser(userDn); + LOG.debug("User {} member of : {}", userDn, memberOf); + } catch (NamingException e) { + throw new AuthenticationException("LDAP Authentication failed for user", e); + } + + for (String groupDn : memberOf) { + String shortName = LdapUtils.getShortName(groupDn); + if (groupFilter.contains(shortName)) { + LOG.info("Authentication succeeded based on group membership"); + return; + } + } + LOG.info("Authentication failed based on user membership"); + throw new AuthenticationException("Authentication failed: " + + "User not a member of specified list"); + } + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/LdapSearch.java service/src/java/org/apache/hive/service/auth/ldap/LdapSearch.java new file mode 100644 index 0000000..fda0131 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/LdapSearch.java @@ -0,0 +1,166 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchResult; +import org.apache.hadoop.hive.conf.HiveConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implements search for LDAP. + */ +public final class LdapSearch implements DirSearch { + + private static final Logger LOG = LoggerFactory.getLogger(LdapSearch.class); + + private final String baseDn; + private final List groupBases; + private final List userBases; + private final List userPatterns; + + private final QueryFactory queries; + + private final DirContext ctx; + + private final Map userDnCache = new HashMap<>(); + + /** + * Construct an instance of {@code LdapSearch}. + * @param conf Hive configuration + * @param ctx Directory service that will be used for the queries. + * @throws NamingException + */ + public LdapSearch(HiveConf conf, DirContext ctx) throws NamingException { + baseDn = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN); + groupBases = LdapUtils.patternsToBaseDns(LdapUtils.parseDnPatterns(conf, + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN)); + userBases = LdapUtils.patternsToBaseDns(LdapUtils.parseDnPatterns(conf, + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN)); + userPatterns = LdapUtils.parseDnPatterns(conf, + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN); + this.ctx = ctx; + queries = new QueryFactory(conf); + } + + /** + * Closes this search object and releases any system resources associated + * with it. If the search object is already closed then invoking this + * method has no effect. + */ + @Override + public void close() { + try { + ctx.close(); + } catch (NamingException e) { + LOG.warn("Exception when closing LDAP context:", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public String findUserDn(String user) throws NamingException { + String userDn = userDnCache.get(user); + + if (userDn != null) { + return userDn; + } + + List allLdapNames; + if (LdapUtils.isDn(user)) { + String userBaseDn = LdapUtils.extractBaseDn(user); + String userRdn = LdapUtils.extractFirstRdn(user); + allLdapNames = execute(Collections.singletonList(userBaseDn), + queries.findUserDnByRdn(userRdn)).getAllLdapNames(); + } else { + allLdapNames = findDnByPattern(userPatterns, user); + if (allLdapNames.isEmpty()) { + allLdapNames = execute(userBases, queries.findUserDnByName(user)).getAllLdapNames(); + } + } + + if (allLdapNames.size() == 1) { + userDn = allLdapNames.get(0); + userDnCache.put(user, userDn); + return userDn; + } else { + LOG.info("Matched multiple users for the user: " + user + ", returning null"); + LOG.debug("Matched users: {}", allLdapNames); + return null; + } + } + + private List findDnByPattern(List patterns, String name) throws NamingException { + for (String pattern : patterns) { + String baseDnFromPattern = LdapUtils.extractBaseDn(pattern); + String rdn = LdapUtils.extractFirstRdn(pattern).replaceAll("%s", name); + List list = execute(Collections.singletonList(baseDnFromPattern), + queries.findDnByPattern(rdn)).getAllLdapNames(); + if (!list.isEmpty()) { + return list; + } + } + return Collections.emptyList(); + } + + /** + * {@inheritDoc} + */ + @Override + public List findGroupsForUser(String userDn) throws NamingException { + String userName = LdapUtils.extractUserName(userDn); + return execute(groupBases, queries.findGroupsForUser(userName, userDn)).getAllLdapNames(); + } + + /** + * {@inheritDoc} + */ + @Override + public List executeCustomQuery(String query) throws NamingException { + return execute(Collections.singletonList(baseDn), queries.customQuery(query)) + .getAllLdapNamesAndAttributes(); + } + + private SearchResultHandler execute(Collection baseDns, Query query) { + List> searchResults = new ArrayList<>(); + for (String aBaseDn : baseDns) { + try { + NamingEnumeration searchResult = ctx.search(aBaseDn, query.getFilter(), + query.getControls()); + if (searchResult != null) { + searchResults.add(searchResult); + } + } catch (NamingException ex) { + LOG.debug("Exception happened for query '" + query.getFilter() + + "' with base DN '" + aBaseDn + "'", ex); + } + } + return new SearchResultHandler(searchResults); + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/LdapSearchFactory.java service/src/java/org/apache/hive/service/auth/ldap/LdapSearchFactory.java new file mode 100644 index 0000000..abde3b5 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/LdapSearchFactory.java @@ -0,0 +1,64 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.Hashtable; +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for LDAP search objects. + */ +public final class LdapSearchFactory implements DirSearchFactory { + + private static final Logger LOG = LoggerFactory.getLogger(LdapSearchFactory.class); + + /** + * {@inheritDoc} + */ + @Override + public DirSearch getInstance(HiveConf conf, String principal, String password) + throws AuthenticationException { + try { + DirContext ctx = createDirContext(conf, principal, password); + return new LdapSearch(conf, ctx); + } catch (NamingException e) { + LOG.debug("Could not connect to the LDAP Server:Authentication failed for {}", principal); + throw new AuthenticationException("Error validating LDAP user", e); + } + } + + private static DirContext createDirContext(HiveConf conf, String principal, String password) + throws NamingException { + Hashtable env = new Hashtable(); + String ldapUrl = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_URL); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_CREDENTIALS, password); + env.put(Context.SECURITY_PRINCIPAL, principal); + LOG.debug("Connecting using principal {} to ldap url {}", principal, ldapUrl); + return new InitialDirContext(env); + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/LdapUtils.java service/src/java/org/apache/hive/service/auth/ldap/LdapUtils.java new file mode 100644 index 0000000..5232e3e --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/LdapUtils.java @@ -0,0 +1,228 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hive.service.ServiceUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Static utility methods related to LDAP authentication module. + */ +public final class LdapUtils { + + private static final Logger LOG = LoggerFactory.getLogger(LdapUtils.class); + + /** + * Extracts a base DN from the provided distinguished name. + *
+ * Example: + *
+ * "ou=CORP,dc=mycompany,dc=com" is the base DN for "cn=user1,ou=CORP,dc=mycompany,dc=com" + * + * @param dn distinguished name + * @return base DN + */ + public static String extractBaseDn(String dn) { + final int indexOfFirstDelimiter = dn.indexOf(","); + if (indexOfFirstDelimiter > -1) { + return dn.substring(indexOfFirstDelimiter + 1); + } + return null; + } + + /** + * Extracts the first Relative Distinguished Name (RDN). + *
+ * Example: + *
+ * For DN "cn=user1,ou=CORP,dc=mycompany,dc=com" this method will return "cn=user1" + * @param dn distinguished name + * @return first RDN + */ + public static String extractFirstRdn(String dn) { + return dn.substring(0, dn.indexOf(",")); + } + + /** + * Extracts username from user DN. + *
+ * Examples: + *
+   * LdapUtils.extractUserName("UserName")                        = "UserName"
+   * LdapUtils.extractUserName("UserName@mycorp.com")             = "UserName"
+   * LdapUtils.extractUserName("cn=UserName,dc=mycompany,dc=com") = "UserName"
+   * 
+ * @param userDn + * @return + */ + public static String extractUserName(String userDn) { + if (!isDn(userDn) && !hasDomain(userDn)) { + return userDn; + } + + int domainIdx = ServiceUtils.indexOfDomainMatch(userDn); + if (domainIdx > 0) { + return userDn.substring(0, domainIdx); + } + + if (userDn.contains("=")) { + return userDn.substring(userDn.indexOf("=") + 1, userDn.indexOf(",")); + } + return userDn; + } + + /** + * Gets value part of the first attribute in the provided RDN. + *
+ * Example: + *
+ * For RDN "cn=user1,ou=CORP" this method will return "user1" + * @param rdn Relative Distinguished Name + * @return value part of the first attribute + */ + public static String getShortName(String rdn) { + return ((rdn.split(","))[0].split("="))[1]; + } + + /** + * Check for a domain part in the provided username. + *
+ * Example: + *
+ *
+   * LdapUtils.hasDomain("user1@mycorp.com") = true
+   * LdapUtils.hasDomain("user1")            = false
+   * 
+ * @param userName username + * @return true if {@code userName} contains {@code @} part + */ + public static boolean hasDomain(String userName) { + return (ServiceUtils.indexOfDomainMatch(userName) > 0); + } + + /** + * Detects DN names. + *
+ * Example: + *
+ *
+   * LdapUtils.isDn("cn=UserName,dc=mycompany,dc=com") = true
+   * LdapUtils.isDn("user1")                           = false
+   * 
+ * @param name name to be checked + * @return true if the provided name is a distinguished name + */ + public static boolean isDn(String name) { + return name.contains("="); + } + + /** + * Reads and parses DN patterns from Hive configuration. + *
+ * If no patterns are provided in the configuration, then the base DN will be used. + * @param conf Hive configuration + * @param var variable to be read + * @return a list of DN patterns + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN + */ + public static List parseDnPatterns(HiveConf conf, HiveConf.ConfVars var) { + String patternsString = conf.getVar(var); + List result = new ArrayList<>(); + if (StringUtils.isBlank(patternsString)) { + String defaultBaseDn = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN); + String guidAttr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY); + if (StringUtils.isNotBlank(defaultBaseDn)) { + result.add(guidAttr + "=%s," + defaultBaseDn); + } + } else { + String[] patterns = patternsString.split(":"); + for (String pattern : patterns) { + if (pattern.contains(",") && pattern.contains("=")) { + result.add(pattern); + } else { + LOG.warn("Unexpected format for " + var + "..ignoring " + pattern); + } + } + } + return result; + } + + private static String patternToBaseDn(String pattern) { + if (pattern.contains("=%s")) { + return pattern.split(",", 2)[1]; + } + return pattern; + } + + /** + * Converts a collection of Distinguished Name patterns to a collection of base DNs. + * @param patterns Distinguished Name patterns + * @return a list of base DNs + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN + */ + public static List patternsToBaseDns(Collection patterns) { + List result = new ArrayList<>(); + for (String pattern : patterns) { + result.add(patternToBaseDn(pattern)); + } + return result; + } + + /** + * Creates a list of principals to be used for user authentication. + * @param conf Hive configuration + * @param user username + * @return a list of user's principals + */ + public static List createCandidatePrincipals(HiveConf conf, String user) { + if (hasDomain(user) || isDn(user)) { + return Collections.singletonList(user); + } + + String ldapDomain = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_DOMAIN); + if (StringUtils.isNotBlank(ldapDomain)) { + return Collections.singletonList(user + "@" + ldapDomain); + } + + List userPatterns = parseDnPatterns(conf, + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN); + if (userPatterns.isEmpty()) { + return Collections.singletonList(user); + } + + List candidatePrincipals = new ArrayList<>(); + for (String userPattern : userPatterns) { + candidatePrincipals.add(userPattern.replaceAll("%s", user)); + } + return candidatePrincipals; + } + + private LdapUtils() { + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/Query.java service/src/java/org/apache/hive/service/auth/ldap/Query.java new file mode 100644 index 0000000..d3c3d9b --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/Query.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.hive.service.auth.ldap; + +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.List; +import javax.naming.directory.SearchControls; +import org.stringtemplate.v4.ST; + +/** + * The object that encompasses all components of a Directory Service search query. + *
+ * @see LdapSearch + */ +public final class Query { + + private final String filter; + private final SearchControls controls; + + /** + * Constructs an instance of Directory Service search query. + * @param filter search filter + * @param controls search controls + */ + public Query(String filter, SearchControls controls) { + this.filter = filter; + this.controls = controls; + } + + /** + * Returns search filter. + * @return search filter + */ + public String getFilter() { + return filter; + } + + /** + * Returns search controls. + * @return search controls + */ + public SearchControls getControls() { + return controls; + } + + /** + * Creates Query Builder. + * @return query builder. + */ + public static QueryBuilder builder() { + return new QueryBuilder(); + } + + /** + * A builder of the {@link Query}. + */ + public static final class QueryBuilder { + + private ST filterTemplate; + private final SearchControls controls = new SearchControls(); + private final List returningAttributes = new ArrayList<>(); + + private QueryBuilder() { + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); + controls.setReturningAttributes(new String[0]); + } + + /** + * Sets search filter template. + * @param filterTemplate search filter template + * @return the current instance of the builder + */ + public QueryBuilder filter(String filterTemplate) { + this.filterTemplate = new ST(filterTemplate); + return this; + } + + /** + * Sets mapping between names in the search filter template and actual values. + * @param key marker in the search filter template. + * @param value actual value + * @return the current instance of the builder + */ + public QueryBuilder map(String key, String value) { + filterTemplate.add(key, value); + return this; + } + + /** + * Sets attribute that should be returned in results for the query. + * @param attributeName attribute name + * @return the current instance of the builder + */ + public QueryBuilder returnAttribute(String attributeName) { + returningAttributes.add(attributeName); + return this; + } + + /** + * Sets the maximum number of entries to be returned as a result of the search. + *
+ * 0 indicates no limit: all entries will be returned. + * @param limit The maximum number of entries that will be returned. + * @return the current instance of the builder + */ + public QueryBuilder limit(int limit) { + controls.setCountLimit(limit); + return this; + } + + private void validate() { + Preconditions.checkArgument(filterTemplate != null, + "filter is required for LDAP search query"); + } + + private String createFilter() { + return filterTemplate.render(); + } + + private void updateControls() { + if (!returningAttributes.isEmpty()) { + controls.setReturningAttributes(returningAttributes + .toArray(new String[returningAttributes.size()])); + } + } + + /** + * Builds an instance of {@link Query}. + * @return configured directory service query + */ + public Query build() { + validate(); + String filter = createFilter(); + updateControls(); + return new Query(filter, controls); + } + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/QueryFactory.java service/src/java/org/apache/hive/service/auth/ldap/QueryFactory.java new file mode 100644 index 0000000..24e4ff6 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/QueryFactory.java @@ -0,0 +1,131 @@ +/** + * 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.hive.service.auth.ldap; + +import com.google.common.base.Strings; +import org.apache.hadoop.hive.conf.HiveConf; + +/** + * A factory for common types of directory service search queries. + */ +public final class QueryFactory { + + private final String guidAttr; + private final String groupClassAttr; + private final String groupMembershipAttr; + + /** + * Constructs the factory based on provided Hive configuration. + * @param conf Hive configuration + */ + public QueryFactory(HiveConf conf) { + guidAttr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY); + groupClassAttr = conf.getVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPCLASS_KEY); + groupMembershipAttr = conf.getVar( + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPMEMBERSHIP_KEY); + } + + /** + * Returns a query for finding Group DN based on group unique ID. + * @param groupId group unique identifier + * @return an instance of {@link Query} + */ + public Query findGroupDnById(String groupId) { + return Query.builder() + .filter("(&(objectClass=)(=))") + .map("guidAttr", guidAttr) + .map("groupClassAttr", groupClassAttr) + .map("groupID", groupId) + .build(); + } + + /** + * Returns a query for finding user DN based on user RDN. + * @param userRdn user RDN + * @return an instance of {@link Query} + */ + public Query findUserDnByRdn(String userRdn) { + return Query.builder() + .filter("(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + + "())") + .map("userRdn", userRdn) + .build(); + } + + /** + * Returns a query for finding user DN based on DN pattern. + *
+ * Name of this method was derived from the original implementation of LDAP authentication. + * This method should be replaced by {@link QueryFactory#findUserDnByRdn(java.lang.String). + * + * @param rdn user RDN + * @return an instance of {@link Query} + */ + public Query findDnByPattern(String rdn) { + return Query.builder() + .filter("()") + .map("rdn", rdn) + .build(); + } + + /** + * Returns a query for finding user DN based on user unique name. + * @param userName user unique name (uid or sAMAccountName) + * @return an instance of {@link Query} + */ + public Query findUserDnByName(String userName) { + return Query.builder() + .filter("(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))" + + "(|(uid=)(sAMAccountName=)))") + .map("userName", userName) + .build(); + } + + /** + * Returns a query for finding groups to which the user belongs. + * @param userName username + * @param userDn user DN + * @return an instance of {@link Query} + */ + public Query findGroupsForUser(String userName, String userDn) { + return Query.builder() + .filter("(&(objectClass=)(|(=)" + + "(=)))") + .map("groupClassAttr", groupClassAttr) + .map("groupMembershipAttr", groupMembershipAttr) + .map("userName", userName) + .map("userDn", userDn) + .build(); + } + + /** + * Returns a query object created for the custom filter. + *
+ * This query is configured to return a group membership attribute as part of the search result. + * @param searchFilter custom search filter + * @return an instance of {@link Query} + */ + public Query customQuery(String searchFilter) { + Query.QueryBuilder builder = Query.builder(); + builder.filter(searchFilter); + if (!Strings.isNullOrEmpty(groupMembershipAttr)) { + builder.returnAttribute(groupMembershipAttr); + } + return builder.build(); + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/SearchResultHandler.java service/src/java/org/apache/hive/service/auth/ldap/SearchResultHandler.java new file mode 100644 index 0000000..ac02458 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/SearchResultHandler.java @@ -0,0 +1,156 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.SearchResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The object that handles Directory Service search results. + * In most cases it converts search results into a list of names in the namespace. + */ +public final class SearchResultHandler { + + private static final Logger LOG = LoggerFactory.getLogger(SearchResultHandler.class); + + private final Collection> searchResults; + + /** + * Constructs a search result handler object for the provided search results. + * @param searchResults directory service search results + */ + public SearchResultHandler(Collection> searchResults) { + this.searchResults = searchResults; + } + + /** + * Returns all entries from the search result. + * @return a list of names in the namespace + * @throws NamingException + */ + public List getAllLdapNames() throws NamingException { + final List result = new ArrayList<>(); + handle(new RecordProcessor() { + @Override + public boolean process(SearchResult record) throws NamingException { + result.add(record.getNameInNamespace()); + return true; + } + }); + return result; + } + + /** + * Checks whether search result contains exactly one entry. + * @return true if the search result contains a single entry. + * @throws NamingException + */ + public boolean hasSingleResult() throws NamingException { + List allResults = getAllLdapNames(); + return allResults != null && allResults.size() == 1; + } + + /** + * Returns a single entry from the search result. + * Throws {@code NamingException} if the search result doesn't contain exactly one entry. + * @return name in the namespace + * @throws NamingException + */ + public String getSingleLdapName() throws NamingException { + List allLdapNames = getAllLdapNames(); + if (allLdapNames.size() == 1) { + return allLdapNames.get(0); + } + throw new NamingException("Single result was expected"); + } + + /** + * Returns all entries and all attributes for these entries. + * @return a list that includes all entries and all attributes from these entries. + * @throws NamingException + */ + public List getAllLdapNamesAndAttributes() throws NamingException { + final List result = new ArrayList<>(); + handle(new RecordProcessor() { + @Override + public boolean process(SearchResult record) throws NamingException { + result.add(record.getNameInNamespace()); + NamingEnumeration allAttributes = record.getAttributes().getAll(); + while(allAttributes.hasMore()) { + Attribute attribute = allAttributes.next(); + result.add((String)attribute.get()); + } + return true; + } + }); + return result; + } + + /** + * Allows for custom processing of the search results. + * @param processor {@link RecordProcessor} implementation + * @throws NamingException + */ + public void handle(RecordProcessor processor) throws NamingException { + try { + for (NamingEnumeration searchResult : searchResults) { + while (searchResult.hasMore()) { + if (!processor.process(searchResult.next())) { + return; + } + } + } + } finally { + for (NamingEnumeration searchResult : searchResults) { + try { + searchResult.close(); + } catch (NamingException ex) { + LOG.warn("Failed to close LDAP search result", ex); + } + } + } + } + + /** + * An interface used by {@link SearchResultHandler} for processing records of + * a {@link SearchResult} on a per-record basis. + *
+ * Implementations of this interface perform the actual work of processing each record, + * but don't need to worry about exception handling, closing underlying data structures, + * and combining results from several search requests. + * {@see SearchResultHandler} + */ + public interface RecordProcessor { + + /** + * Implementations must implement this method to process each record in {@link SearchResult}. + * @param record the {@code SearchResult} to precess + * @return {@code true} to continue processing, {@code false} to stop iterating + * over search results + * @throws NamingException + */ + boolean process(SearchResult record) throws NamingException; + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/UserFilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/UserFilterFactory.java new file mode 100644 index 0000000..5a91235 --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/UserFilterFactory.java @@ -0,0 +1,75 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A factory for a {@link Filter} based on a list of allowed users. + *
+ * The produced filter object filters out all users that are not on the provided in + * Hive configuration list. + * @see HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER + */ +public final class UserFilterFactory implements FilterFactory { + + /** + * {@inheritDoc} + */ + @Override + public Filter getInstance(HiveConf conf) { + Collection userFilter = conf.getStringCollection( + HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER.varname); + + if (userFilter.isEmpty()) { + return null; + } + + return new UserFilter(userFilter); + } + + private static final class UserFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(UserFilter.class); + + private final Set userFilter = new HashSet<>(); + + UserFilter(Collection userFilter) { + for (String userFilterItem : userFilter) { + this.userFilter.add(userFilterItem.toLowerCase()); + } + } + + @Override + public void apply(DirSearch ldap, String user) throws AuthenticationException { + LOG.info("Authenticating user '{}' using user filter", user); + String userName = LdapUtils.extractUserName(user).toLowerCase(); + if (!userFilter.contains(userName)) { + LOG.info("Authentication failed based on user membership"); + throw new AuthenticationException("Authentication failed: " + + "User not a member of specified list"); + } + } + } +} diff --git service/src/java/org/apache/hive/service/auth/ldap/UserSearchFilterFactory.java service/src/java/org/apache/hive/service/auth/ldap/UserSearchFilterFactory.java new file mode 100644 index 0000000..60801bb --- /dev/null +++ service/src/java/org/apache/hive/service/auth/ldap/UserSearchFilterFactory.java @@ -0,0 +1,55 @@ +/** + * 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.hive.service.auth.ldap; + +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; + +/** + * A factory for a {@link Filter} that check whether provided user could be found in the directory. + *
+ * The produced filter object filters out all users that are not found in the directory. + */ +public final class UserSearchFilterFactory implements FilterFactory { + + /** + * {@inheritDoc} + */ + @Override + public Filter getInstance(HiveConf conf) { + return new UserSearchFilter(); + } + + private static final class UserSearchFilter implements Filter { + @Override + public void apply(DirSearch client, String user) throws AuthenticationException { + try { + String userDn = client.findUserDn(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"); + } + } catch (NamingException e) { + throw new AuthenticationException("LDAP Authentication failed for user", e); + } + } + } +} diff --git service/src/test/org/apache/hive/service/auth/TestLdapAtnProviderWithMiniDS.java service/src/test/org/apache/hive/service/auth/TestLdapAtnProviderWithMiniDS.java index 089a059..23a048a 100644 --- service/src/test/org/apache/hive/service/auth/TestLdapAtnProviderWithMiniDS.java +++ service/src/test/org/apache/hive/service/auth/TestLdapAtnProviderWithMiniDS.java @@ -225,7 +225,6 @@ public static void init() throws Exception { hiveConf = new HiveConf(); ldapProvider = new LdapAuthenticationProviderImpl(hiveConf); - ldapProvider.init(hiveConf); } @AfterClass @@ -259,7 +258,7 @@ private static void initLdapAtn(Map hiveProperties) } } - ldapProvider.init(hiveConf); + ldapProvider = new LdapAuthenticationProviderImpl(hiveConf); } @Test diff --git service/src/test/org/apache/hive/service/auth/TestLdapAuthenticationProviderImpl.java service/src/test/org/apache/hive/service/auth/TestLdapAuthenticationProviderImpl.java index f276906..fd6ef44 100644 --- service/src/test/org/apache/hive/service/auth/TestLdapAuthenticationProviderImpl.java +++ service/src/test/org/apache/hive/service/auth/TestLdapAuthenticationProviderImpl.java @@ -15,51 +15,240 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hive.service.auth; +import java.io.IOException; +import java.util.Arrays; +import javax.naming.NamingException; import javax.security.sasl.AuthenticationException; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; - -import junit.framework.TestCase; import org.apache.hadoop.hive.conf.HiveConf; +import org.apache.hive.service.auth.ldap.DirSearch; +import org.apache.hive.service.auth.ldap.DirSearchFactory; +import org.apache.hive.service.auth.ldap.LdapSearchFactory; +import org.junit.Test; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestLdapAuthenticationProviderImpl { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + public HiveConf conf; + public LdapAuthenticationProviderImpl auth; + + @Mock + public DirSearchFactory factory; + + @Mock + public DirSearch search; + + @Before + public void setup() throws AuthenticationException { + conf = new HiveConf(); + conf.set("hive.root.logger", "DEBUG,console"); + conf.set("hive.server2.authentication.ldap.url", "localhost"); + when(factory.getInstance(any(HiveConf.class), anyString(), anyString())).thenReturn(search); + } + + @Test + public void authenticateGivenBlankPassword() throws Exception { + auth = new LdapAuthenticationProviderImpl(conf, new LdapSearchFactory()); + expectAuthenticationExceptionForInvalidPassword(); + auth.Authenticate("user", ""); + } + + @Test + public void authenticateGivenStringWithNullCharacterForPassword() throws Exception { + auth = new LdapAuthenticationProviderImpl(conf, new LdapSearchFactory()); + expectAuthenticationExceptionForInvalidPassword(); + auth.Authenticate("user", "\0"); + } + + @Test + public void authenticateGivenNullForPassword() throws Exception { + auth = new LdapAuthenticationProviderImpl(conf, new LdapSearchFactory()); + expectAuthenticationExceptionForInvalidPassword(); + auth.Authenticate("user", null); + } + + @Test + public void testAuthenticateNoUserOrGroupFilter() throws NamingException, AuthenticationException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "cn=%s,ou=Users,dc=mycorp,dc=com:cn=%s,ou=PowerUsers,dc=mycorp,dc=com"); -public class TestLdapAuthenticationProviderImpl extends TestCase { + DirSearchFactory factory = mock(DirSearchFactory.class); - private static HiveConf hiveConf; - private static byte[] hiveConfBackup; + when(search.findUserDn("user1")).thenReturn("cn=user1,ou=PowerUsers,dc=mycorp,dc=com"); + + when(factory.getInstance(conf, "cn=user1,ou=PowerUsers,dc=mycorp,dc=com", "Blah")).thenReturn(search); + when(factory.getInstance(conf, "cn=user1,ou=Users,dc=mycorp,dc=com", "Blah")).thenThrow(AuthenticationException.class); - @Override - public void setUp() throws Exception { - hiveConf = new HiveConf(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - hiveConf.writeXml(baos); - baos.close(); - hiveConfBackup = baos.toByteArray(); - hiveConf.set("hive.server2.authentication.ldap.url", "localhost"); - FileOutputStream fos = new FileOutputStream(new File(hiveConf.getHiveSiteLocation().toURI())); - hiveConf.writeXml(fos); - fos.close(); + auth = new LdapAuthenticationProviderImpl(conf, factory); + auth.Authenticate("user1", "Blah"); + + verify(factory, times(2)).getInstance(isA(HiveConf.class), anyString(), eq("Blah")); + verify(search, atLeastOnce()).close(); + } + + @Test + public void testAuthenticateWhenUserFilterPasses() throws NamingException, AuthenticationException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, + "user1,user2"); + + when(search.findUserDn("user1")).thenReturn("cn=user1,ou=PowerUsers,dc=mycorp,dc=com"); + when(search.findUserDn("user2")).thenReturn("cn=user2,ou=PowerUsers,dc=mycorp,dc=com"); + + authenticateUserAndCheckSearchIsClosed("user1"); + authenticateUserAndCheckSearchIsClosed("user2"); + } + + @Test + public void testAuthenticateWhenUserSearchFails() throws NamingException, AuthenticationException, IOException { + thrown.expect(AuthenticationException.class); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user1,user2"); + + when(search.findUserDn("user1")).thenReturn(null); + + authenticateUserAndCheckSearchIsClosed("user1"); + } + + @Test + public void testAuthenticateWhenUserFilterFails() throws NamingException, AuthenticationException, IOException { + thrown.expect(AuthenticationException.class); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user1,user2"); + + when(search.findUserDn("user3")).thenReturn("cn=user3,ou=PowerUsers,dc=mycorp,dc=com"); + + authenticateUserAndCheckSearchIsClosed("user3"); + } + + @Test + public void testAuthenticateWhenGroupFilterPasses() throws NamingException, AuthenticationException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "group1,group2"); + + when(search.findUserDn("user1")).thenReturn("cn=user1,ou=PowerUsers,dc=mycorp,dc=com"); + when(search.findUserDn("user2")).thenReturn("cn=user2,ou=PowerUsers,dc=mycorp,dc=com"); + + when(search.findGroupsForUser("cn=user1,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=group1,ou=Groups,dc=mycorp,dc=com")); + when(search.findGroupsForUser("cn=user2,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=group2,ou=Groups,dc=mycorp,dc=com")); + + authenticateUserAndCheckSearchIsClosed("user1"); + authenticateUserAndCheckSearchIsClosed("user2"); } + + @Test + public void testAuthenticateWhenUserAndGroupFiltersPass() throws NamingException, AuthenticationException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "group1,group2"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user1,user2"); + + when(search.findUserDn("user1")).thenReturn("cn=user1,ou=PowerUsers,dc=mycorp,dc=com"); + when(search.findUserDn("user2")).thenReturn("cn=user2,ou=PowerUsers,dc=mycorp,dc=com"); + + when(search.findGroupsForUser("cn=user1,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=group1,ou=Groups,dc=mycorp,dc=com")); + when(search.findGroupsForUser("cn=user2,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=group2,ou=Groups,dc=mycorp,dc=com")); - public void testLdapEmptyPassword() { - LdapAuthenticationProviderImpl ldapImpl = new LdapAuthenticationProviderImpl(hiveConf); + authenticateUserAndCheckSearchIsClosed("user1"); + authenticateUserAndCheckSearchIsClosed("user2"); + } + + @Test + public void testAuthenticateWhenUserFilterPassesAndGroupFilterFails() + throws NamingException, AuthenticationException, IOException { + thrown.expect(AuthenticationException.class); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "group1,group2"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user1,user2"); + + when(search.findUserDn("user1")).thenReturn("cn=user1,ou=PowerUsers,dc=mycorp,dc=com"); + + when(search.findGroupsForUser("cn=user1,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=OtherGroup,ou=Groups,dc=mycorp,dc=com")); + + authenticateUserAndCheckSearchIsClosed("user1"); + } + + @Test + public void testAuthenticateWhenUserFilterFailsAndGroupFilterPasses() + throws NamingException, AuthenticationException, IOException { + thrown.expect(AuthenticationException.class); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "group3"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user1,user2"); + + when(search.findUserDn("user3")).thenReturn("cn=user3,ou=PowerUsers,dc=mycorp,dc=com"); + + when(search.findGroupsForUser("cn=user3,ou=PowerUsers,dc=mycorp,dc=com")) + .thenReturn(Arrays.asList( + "cn=testGroup,ou=Groups,dc=mycorp,dc=com", + "cn=group3,ou=Groups,dc=mycorp,dc=com")); + + authenticateUserAndCheckSearchIsClosed("user3"); + } + + @Test + public void testAuthenticateWhenCustomQueryFilterPasses() throws NamingException, AuthenticationException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN, "dc=mycorp,dc=com"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY, + "(&(objectClass=person)(|(memberOf=CN=Domain Admins,CN=Users,DC=apache,DC=org)(memberOf=CN=Administrators,CN=Builtin,DC=apache,DC=org)))"); + + when(search.executeCustomQuery(anyString())).thenReturn(Arrays.asList( + "cn=user1,ou=PowerUsers,dc=mycorp,dc=com", + "cn=user2,ou=PowerUsers,dc=mycorp,dc=com")); + + authenticateUserAndCheckSearchIsClosed("user1"); + } + + @Test + public void testAuthenticateWhenCustomQueryFilterFailsAndUserFilterPasses() throws NamingException, AuthenticationException, IOException { + thrown.expect(AuthenticationException.class); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN, "dc=mycorp,dc=com"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY, + "(&(objectClass=person)(|(memberOf=CN=Domain Admins,CN=Users,DC=apache,DC=org)(memberOf=CN=Administrators,CN=Builtin,DC=apache,DC=org)))"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "user3"); + + when(search.findUserDn("user3")).thenReturn("cn=user3,ou=PowerUsers,dc=mycorp,dc=com"); + when(search.executeCustomQuery(anyString())).thenReturn(Arrays.asList( + "cn=user1,ou=PowerUsers,dc=mycorp,dc=com", + "cn=user2,ou=PowerUsers,dc=mycorp,dc=com")); + + authenticateUserAndCheckSearchIsClosed("user3"); + } + + private void expectAuthenticationExceptionForInvalidPassword() { + thrown.expect(AuthenticationException.class); + thrown.expectMessage("a null or blank password has been provided"); + } + + private void authenticateUserAndCheckSearchIsClosed(String user) throws IOException { + auth = new LdapAuthenticationProviderImpl(conf, factory); try { - ldapImpl.Authenticate("user", ""); - assertFalse(true); - } catch (AuthenticationException e) { - assertTrue(e.getMessage(), e.getMessage().contains("a null or blank password has been provided")); - } - } - - @Override - public void tearDown() throws Exception { - if(hiveConf != null && hiveConfBackup != null) { - FileOutputStream fos = new FileOutputStream(new File(hiveConf.getHiveSiteLocation().toURI())); - fos.write(hiveConfBackup); - fos.close(); - } + auth.Authenticate(user, "password doesn't matter"); + } finally { + verify(search, atLeastOnce()).close(); + } } } diff --git service/src/test/org/apache/hive/service/auth/ldap/Credentials.java service/src/test/org/apache/hive/service/auth/ldap/Credentials.java new file mode 100644 index 0000000..ce22b8e --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/Credentials.java @@ -0,0 +1,41 @@ +/** + * 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.hive.service.auth.ldap; + +public final class Credentials { + + private final String user; + private final String password; + + private Credentials(String user, String password) { + this.user = user; + this.password = password; + } + + public static Credentials of(String user, String password) { + return new Credentials(user, password); + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/LdapTestUtils.java service/src/test/org/apache/hive/service/auth/ldap/LdapTestUtils.java new file mode 100644 index 0000000..ab96f9a --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/LdapTestUtils.java @@ -0,0 +1,116 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.SearchResult; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.mockito.stubbing.OngoingStubbing; + +public final class LdapTestUtils { + + private LdapTestUtils() { + } + + public static NamingEnumeration mockEmptyNamingEnumeration() throws NamingException { + return mockNamingEnumeration(new SearchResult[0]); + } + + public static NamingEnumeration mockNamingEnumeration(String... dns) throws NamingException { + return mockNamingEnumeration(mockSearchResults(dns).toArray(new SearchResult[0])); + } + + public static NamingEnumeration mockNamingEnumeration(SearchResult... searchResults) throws NamingException { + NamingEnumeration ne = + (NamingEnumeration) mock(NamingEnumeration.class); + mockHasMoreMethod(ne, searchResults.length); + if (searchResults.length > 0) { + List mockedResults = Arrays.asList(searchResults); + mockNextMethod(ne, mockedResults); + } + return ne; + } + + public static void mockHasMoreMethod(NamingEnumeration ne, int length) throws NamingException { + OngoingStubbing hasMoreStub = when(ne.hasMore()); + for (int i = 0; i < length; i++) { + hasMoreStub = hasMoreStub.thenReturn(true); + } + hasMoreStub.thenReturn(false); + } + + public static void mockNextMethod(NamingEnumeration ne, List searchResults) throws NamingException { + OngoingStubbing nextStub = when(ne.next()); + for (SearchResult searchResult : searchResults) { + nextStub = nextStub.thenReturn(searchResult); + } + } + + public static List mockSearchResults(String[] dns) { + List list = new ArrayList<>(); + for (String dn : dns) { + list.add(mockSearchResult(dn, null)); + } + return list; + } + + public static SearchResult mockSearchResult(String dn, Attributes attributes) { + SearchResult searchResult = mock(SearchResult.class); + when(searchResult.getNameInNamespace()).thenReturn(dn); + when(searchResult.getAttributes()).thenReturn(attributes); + return searchResult; + } + + public static Attributes mockEmptyAttributes() throws NamingException { + return mockAttributes(); + } + + public static Attributes mockAttributes(String name, String value) throws NamingException { + return mockAttributes(new NameValue(name, value)); + } + + public static Attributes mockAttributes(String name1, String value1, String name2, String value2) throws NamingException { + return mockAttributes(new NameValue(name1, value1), new NameValue(name2, value2)); + } + + private static Attributes mockAttributes(NameValue... namedValues) throws NamingException { + Attributes attributes = new BasicAttributes(); + for (NameValue namedValue : namedValues) { + attributes.put(namedValue.name, namedValue.value); + } + return attributes; + } + + private static final class NameValue { + final String name; + final String value; + + public NameValue(String name, String value) { + this.name = name; + this.value = value; + } + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestChainFilter.java service/src/test/org/apache/hive/service/auth/ldap/TestChainFilter.java new file mode 100644 index 0000000..de34ab09 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestChainFilter.java @@ -0,0 +1,103 @@ +/** + * 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.hive.service.auth.ldap; + +import java.io.IOException; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import org.junit.Before; +import org.mockito.Mock; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestChainFilter { + + private FilterFactory factory; + private HiveConf conf; + + @Mock + public Filter filter1; + + @Mock + public Filter filter2; + + @Mock + public Filter filter3; + + @Mock + public FilterFactory factory1; + + @Mock + public FilterFactory factory2; + + @Mock + public FilterFactory factory3; + + @Mock + private DirSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + factory = new ChainFilterFactory(factory1, factory2, factory3); + } + + @Test + public void testFactoryAllNull() { + assertNull(factory.getInstance(conf)); + } + + @Test + public void testFactoryAllEmpty() { + FilterFactory emptyFactory = new ChainFilterFactory(); + assertNull(emptyFactory.getInstance(conf)); + } + + @Test + public void testFactory() throws AuthenticationException { + when(factory1.getInstance(any(HiveConf.class))).thenReturn(filter1); + when(factory2.getInstance(any(HiveConf.class))).thenReturn(filter2); + when(factory3.getInstance(any(HiveConf.class))).thenReturn(filter3); + + Filter filter = factory.getInstance(conf); + + filter.apply(search, "User"); + verify(filter1, times(1)).apply(search, "User"); + verify(filter2, times(1)).apply(search, "User"); + verify(filter3, times(1)).apply(search, "User"); + } + + @Test(expected = AuthenticationException.class) + public void testApplyNegative() throws AuthenticationException, NamingException, IOException { + doThrow(AuthenticationException.class).when(filter3).apply((DirSearch) anyObject(), anyString()); + + when(factory1.getInstance(any(HiveConf.class))).thenReturn(filter1); + when(factory3.getInstance(any(HiveConf.class))).thenReturn(filter3); + + Filter filter = factory.getInstance(conf); + + filter.apply(search, "User"); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestCustomQueryFilter.java service/src/test/org/apache/hive/service/auth/ldap/TestCustomQueryFilter.java new file mode 100644 index 0000000..e4514e1 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestCustomQueryFilter.java @@ -0,0 +1,85 @@ +/** + * 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.hive.service.auth.ldap; + +import java.io.IOException; +import java.util.Arrays; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import org.junit.Before; +import org.mockito.Mock; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestCustomQueryFilter { + + private static final String USER2_DN = "uid=user2,ou=People,dc=example,dc=com"; + private static final String USER1_DN = "uid=user1,ou=People,dc=example,dc=com"; + private static final String CUSTOM_QUERY = "(&(objectClass=person)(|(uid=user1)(uid=user2)))"; + + private FilterFactory factory; + private HiveConf conf; + + @Mock + private DirSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + conf.set("hive.root.logger", "DEBUG,console"); + factory = new CustomQueryFilterFactory(); + } + + @Test + public void testFactory() { + conf.unset(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY.varname); + assertNull(factory.getInstance(conf)); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY, CUSTOM_QUERY); + assertNotNull(factory.getInstance(conf)); + } + + @Test + public void testApplyPositive() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY, CUSTOM_QUERY); + + when(search.executeCustomQuery(eq(CUSTOM_QUERY))).thenReturn(Arrays.asList(USER1_DN, USER2_DN)); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "user1"); + filter.apply(search, "user2"); + } + + + @Test(expected = AuthenticationException.class) + public void testApplyNegative() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_CUSTOMLDAPQUERY, CUSTOM_QUERY); + + when(search.executeCustomQuery(eq(CUSTOM_QUERY))).thenReturn(Arrays.asList(USER1_DN, USER2_DN)); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "user3"); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestGroupFilter.java service/src/test/org/apache/hive/service/auth/ldap/TestGroupFilter.java new file mode 100644 index 0000000..7932f26 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestGroupFilter.java @@ -0,0 +1,101 @@ +/** + * 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.hive.service.auth.ldap; + +import java.io.IOException; +import java.util.Arrays; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import org.junit.Before; +import org.mockito.Mock; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestGroupFilter { + + private FilterFactory factory; + private HiveConf conf; + + @Mock + private DirSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + conf.set("hive.root.logger", "DEBUG,console"); + factory = new GroupFilterFactory(); + } + + @Test + public void testFactory() { + conf.unset(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER.varname); + assertNull(factory.getInstance(conf)); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "G1"); + assertNotNull(factory.getInstance(conf)); + } + + @Test + public void testApplyPositive() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "HiveUsers"); + + when(search.findUserDn(eq("user1"))) + .thenReturn("cn=user1,ou=People,dc=example,dc=com"); + when(search.findUserDn(eq("cn=user2,dc=example,dc=com"))) + .thenReturn("cn=user2,ou=People,dc=example,dc=com"); + when(search.findUserDn(eq("user3@mydomain.com"))) + .thenReturn("cn=user3,ou=People,dc=example,dc=com"); + + when(search.findGroupsForUser(eq("cn=user1,ou=People,dc=example,dc=com"))) + .thenReturn(Arrays.asList( + "cn=SuperUsers,ou=Groups,dc=example,dc=com", + "cn=Office1,ou=Groups,dc=example,dc=com", + "cn=HiveUsers,ou=Groups,dc=example,dc=com", + "cn=G1,ou=Groups,dc=example,dc=com")); + when(search.findGroupsForUser(eq("cn=user2,ou=People,dc=example,dc=com"))) + .thenReturn(Arrays.asList( + "cn=HiveUsers,ou=Groups,dc=example,dc=com")); + when(search.findGroupsForUser(eq("cn=user3,ou=People,dc=example,dc=com"))) + .thenReturn(Arrays.asList( + "cn=HiveUsers,ou=Groups,dc=example,dc=com", + "cn=G1,ou=Groups,dc=example,dc=com", + "cn=G2,ou=Groups,dc=example,dc=com")); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "user1"); + filter.apply(search, "cn=user2,dc=example,dc=com"); + filter.apply(search, "user3@mydomain.com"); + } + + @Test(expected = AuthenticationException.class) + public void testApplyNegative() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPFILTER, "HiveUsers"); + + when(search.findGroupsForUser(eq("user1"))).thenReturn(Arrays.asList("SuperUsers", "Office1", "G1", "G2")); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "user1"); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestLdapSearch.java service/src/test/org/apache/hive/service/auth/ldap/TestLdapSearch.java new file mode 100644 index 0000000..d6228cd --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestLdapSearch.java @@ -0,0 +1,209 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.apache.hive.service.auth.ldap.LdapTestUtils.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestLdapSearch { + + @Mock + private DirContext ctx; + + private HiveConf conf; + private LdapSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + } + + @Test + public void testClose() throws NamingException { + search = new LdapSearch(conf, ctx); + search.close(); + verify(ctx, atLeastOnce()).close(); + } + + @Test + public void testFindUserDnWhenUserDnPositive() throws NamingException { + NamingEnumeration searchResult = mockNamingEnumeration("CN=User1,OU=org1,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(searchResult) + .thenThrow(NamingException.class); + search = new LdapSearch(conf, ctx); + String expected = "CN=User1,OU=org1,DC=foo,DC=bar"; + String actual = search.findUserDn("CN=User1,OU=org1"); + assertEquals(expected, actual); + } + + @Test + public void testFindUserDnWhenUserDnNegativeDuplicates() throws NamingException { + NamingEnumeration searchResult = mockNamingEnumeration( + "CN=User1,OU=org1,DC=foo,DC=bar", + "CN=User1,OU=org2,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))).thenReturn(searchResult); + search = new LdapSearch(conf, ctx); + assertNull(search.findUserDn("CN=User1,DC=foo,DC=bar")); + } + + @Test + public void testFindUserDnWhenUserDnNegativeNone() throws NamingException { + NamingEnumeration searchResult = mockEmptyNamingEnumeration(); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))).thenReturn(searchResult); + search = new LdapSearch(conf, ctx); + assertNull(search.findUserDn("CN=User1,DC=foo,DC=bar")); + } + + @Test + public void testFindUserDnWhenUserPatternFoundBySecondPattern() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar:CN=%s,OU=org2,DC=foo,DC=bar"); + NamingEnumeration emptyResult = mockEmptyNamingEnumeration(); + NamingEnumeration validResult = mockNamingEnumeration("CN=User1,OU=org2,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(emptyResult) + .thenReturn(validResult); + search = new LdapSearch(conf, ctx); + String expected = "CN=User1,OU=org2,DC=foo,DC=bar"; + String actual = search.findUserDn("User1"); + assertEquals(expected, actual); + verify(ctx).search(eq("OU=org1,DC=foo,DC=bar"), contains("CN=User1"), any(SearchControls.class)); + verify(ctx).search(eq("OU=org2,DC=foo,DC=bar"), contains("CN=User1"), any(SearchControls.class)); + } + + @Test + public void testFindUserDnWhenUserPatternFoundByFirstPattern() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar:CN=%s,OU=org2,DC=foo,DC=bar"); + NamingEnumeration emptyResult = mockEmptyNamingEnumeration(); + NamingEnumeration validResult = mockNamingEnumeration("CN=User1,OU=org2,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(validResult) + .thenReturn(emptyResult); + search = new LdapSearch(conf, ctx); + String expected = "CN=User1,OU=org2,DC=foo,DC=bar"; + String actual = search.findUserDn("User1"); + assertEquals(expected, actual); + verify(ctx).search(eq("OU=org1,DC=foo,DC=bar"), contains("CN=User1"), any(SearchControls.class)); + } + + @Test + public void testFindUserDnWhenUserPatternFoundByUniqueIdentifier() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar"); + NamingEnumeration validResult = mockNamingEnumeration("CN=User1,OU=org1,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(null) + .thenReturn(validResult); + search = new LdapSearch(conf, ctx); + String expected = "CN=User1,OU=org1,DC=foo,DC=bar"; + String actual = search.findUserDn("User1"); + assertEquals(expected, actual); + verify(ctx).search(eq("OU=org1,DC=foo,DC=bar"), contains("CN=User1"), any(SearchControls.class)); + verify(ctx).search(eq("OU=org1,DC=foo,DC=bar"), contains("uid=User1"), any(SearchControls.class)); + } + + @Test + public void testFindUserDnWhenUserPatternFoundByUniqueIdentifierNegativeNone() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(null) + .thenReturn(null); + search = new LdapSearch(conf, ctx); + assertNull(search.findUserDn("User1")); + } + + @Test + public void testFindUserDnWhenUserPatternFoundByUniqueIdentifierNegativeMany() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar"); + NamingEnumeration manyResult = mockNamingEnumeration( + "CN=User1,OU=org1,DC=foo,DC=bar", + "CN=User12,OU=org1,DC=foo,DC=bar"); + when(ctx.search(anyString(), anyString(), any(SearchControls.class))) + .thenReturn(null) + .thenReturn(manyResult); + search = new LdapSearch(conf, ctx); + assertNull(search.findUserDn("User1")); + } + + @Test + public void testFindGroupsForUser() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPDNPATTERN, + "CN=%s,OU=org1,DC=foo,DC=bar"); + + NamingEnumeration groupsResult = mockNamingEnumeration("CN=Group1,OU=org1,DC=foo,DC=bar"); + when(ctx.search(eq("OU=org1,DC=foo,DC=bar"), contains("User1"), any(SearchControls.class))) + .thenReturn(groupsResult); + + search = new LdapSearch(conf, ctx); + + List expected = Arrays.asList("CN=Group1,OU=org1,DC=foo,DC=bar"); + List actual = search.findGroupsForUser("CN=User1,OU=org1,DC=foo,DC=bar"); + assertEquals(expected, actual); + } + + @Test + public void testExecuteCustomQuery() throws NamingException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN, "dc=example,dc=com"); + + NamingEnumeration customQueryResult = mockNamingEnumeration( + mockSearchResult( + "uid=group1,ou=Groups,dc=example,dc=com", + mockAttributes("member", "uid=user1,ou=People,dc=example,dc=com")), + mockSearchResult( + "uid=group2,ou=Groups,dc=example,dc=com", + mockAttributes("member", "uid=user2,ou=People,dc=example,dc=com")) + ); + + when(ctx.search(eq("dc=example,dc=com"), anyString(), any(SearchControls.class))) + .thenReturn(customQueryResult); + + search = new LdapSearch(conf, ctx); + + List expected = Arrays.asList( + "uid=group1,ou=Groups,dc=example,dc=com", + "uid=user1,ou=People,dc=example,dc=com", + "uid=group2,ou=Groups,dc=example,dc=com", + "uid=user2,ou=People,dc=example,dc=com"); + List actual = search.executeCustomQuery("(&(objectClass=groupOfNames)(|(cn=group1)(cn=group2)))"); + Collections.sort(expected); + Collections.sort(actual); + assertEquals(expected, actual); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestLdapUtils.java service/src/test/org/apache/hive/service/auth/ldap/TestLdapUtils.java new file mode 100644 index 0000000..07d3e81 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestLdapUtils.java @@ -0,0 +1,103 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TestLdapUtils { + + @Test + public void testCreateCandidatePrincipalsForUserDn() { + HiveConf conf = new HiveConf(); + String userDn = "cn=user1,ou=CORP,dc=mycompany,dc=com"; + List expected = Arrays.asList(userDn); + List actual = LdapUtils.createCandidatePrincipals(conf, userDn); + assertEquals(expected, actual); + } + + @Test + public void testCreateCandidatePrincipalsForUserWithDomain() { + HiveConf conf = new HiveConf(); + String userWithDomain = "user1@mycompany.com"; + List expected = Arrays.asList(userWithDomain); + List actual = LdapUtils.createCandidatePrincipals(conf, userWithDomain); + assertEquals(expected, actual); + } + + @Test + public void testCreateCandidatePrincipalsLdapDomain() { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_DOMAIN, "mycompany.com"); + List expected = Arrays.asList("user1@mycompany.com"); + List actual = LdapUtils.createCandidatePrincipals(conf, "user1"); + assertEquals(expected, actual); + } + + @Test + public void testCreateCandidatePrincipalsUserPatternsDefaultBaseDn() { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY, "sAMAccountName"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN, "dc=mycompany,dc=com"); + List expected = Arrays.asList("sAMAccountName=user1,dc=mycompany,dc=com"); + List actual = LdapUtils.createCandidatePrincipals(conf, "user1"); + assertEquals(expected, actual); + } + + @Test + public void testCreateCandidatePrincipals() { + HiveConf conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_BASEDN, "dc=mycompany,dc=com"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERDNPATTERN, + "cn=%s,ou=CORP1,dc=mycompany,dc=com:cn=%s,ou=CORP2,dc=mycompany,dc=com"); + List expected = Arrays.asList( + "cn=user1,ou=CORP1,dc=mycompany,dc=com", + "cn=user1,ou=CORP2,dc=mycompany,dc=com"); + List actual = LdapUtils.createCandidatePrincipals(conf, "user1"); + Collections.sort(expected); + Collections.sort(actual); + assertEquals(expected, actual); + } + + @Test + public void testExtractFirstRdn() { + String dn = "cn=user1,ou=CORP1,dc=mycompany,dc=com"; + String expected = "cn=user1"; + String actual = LdapUtils.extractFirstRdn(dn); + assertEquals(expected, actual); + } + + @Test + public void testExtractBaseDn() { + String dn = "cn=user1,ou=CORP1,dc=mycompany,dc=com"; + String expected = "ou=CORP1,dc=mycompany,dc=com"; + String actual = LdapUtils.extractBaseDn(dn); + assertEquals(expected, actual); + } + + @Test + public void testExtractBaseDnNegative() { + String dn = "cn=user1"; + assertNull(LdapUtils.extractBaseDn(dn)); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestQuery.java service/src/test/org/apache/hive/service/auth/ldap/TestQuery.java new file mode 100644 index 0000000..6db866f --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestQuery.java @@ -0,0 +1,59 @@ +/** + * 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.hive.service.auth.ldap; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TestQuery { + + @Test + public void testQueryBuilderFilter() { + Query q = Query.builder() + .filter("test = query") + .map("uid_attr", "uid") + .map("value", "Hello!") + .build(); + assertEquals("test uid=Hello! query", q.getFilter()); + assertEquals(0, q.getControls().getCountLimit()); + } + + @Test + public void testQueryBuilderLimit() { + Query q = Query.builder() + .filter(",") + .map("key1", "value1") + .map("key2", "value2") + .limit(8) + .build(); + assertEquals("value1,value2", q.getFilter()); + assertEquals(8, q.getControls().getCountLimit()); + } + + @Test + public void testQueryBuilderReturningAttributes() { + Query q = Query.builder() + .filter("(query)") + .returnAttribute("attr1") + .returnAttribute("attr2") + .build(); + assertEquals("(query)", q.getFilter()); + assertArrayEquals(new String[] {"attr1", "attr2"}, q.getControls().getReturningAttributes()); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestQueryFactory.java service/src/test/org/apache/hive/service/auth/ldap/TestQueryFactory.java new file mode 100644 index 0000000..90a4b55 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestQueryFactory.java @@ -0,0 +1,79 @@ +/** + * 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.hive.service.auth.ldap; + +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TestQueryFactory { + + private QueryFactory queries; + private HiveConf conf; + + @Before + public void setup() { + conf = new HiveConf(); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GUIDKEY, "guid"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPCLASS_KEY, "superGroups"); + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_GROUPMEMBERSHIP_KEY, "member"); + queries = new QueryFactory(conf); + } + + @Test + public void testFindGroupDnById() { + Query q = queries.findGroupDnById("unique_group_id"); + String expected = "(&(objectClass=superGroups)(guid=unique_group_id))"; + String actual = q.getFilter(); + assertEquals(expected, actual); + } + + @Test + public void testFindUserDnByRdn() { + Query q = queries.findUserDnByRdn("cn=User1"); + String expected = "(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))(cn=User1))"; + String actual = q.getFilter(); + assertEquals(expected, actual); + } + + @Test + public void testFindDnByPattern() { + Query q = queries.findDnByPattern("cn=User1"); + String expected = "(cn=User1)"; + String actual = q.getFilter(); + assertEquals(expected, actual); + } + + @Test + public void testFindUserDnByName() { + Query q = queries.findUserDnByName("unique_user_id"); + String expected = "(&(|(objectClass=person)(objectClass=user)(objectClass=inetOrgPerson))(|(uid=unique_user_id)(sAMAccountName=unique_user_id)))"; + String actual = q.getFilter(); + assertEquals(expected, actual); + } + + @Test + public void testFindGroupsForUser() { + Query q = queries.findGroupsForUser("user_name", "user_Dn"); + String expected = "(&(objectClass=superGroups)(|(member=user_Dn)(member=user_name)))"; + String actual = q.getFilter(); + assertEquals(expected, actual); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestSearchResultHandler.java service/src/test/org/apache/hive/service/auth/ldap/TestSearchResultHandler.java new file mode 100644 index 0000000..9a5a86f --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestSearchResultHandler.java @@ -0,0 +1,218 @@ +/** + * 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.hive.service.auth.ldap; + +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.SearchResult; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.apache.hive.service.auth.ldap.LdapTestUtils.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestSearchResultHandler { + + SearchResultHandler handler; + + @Test + public void testHandle() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResultWithDns("1") + .addSearchResultWithDns("2", "3"); + handler = new SearchResultHandler(resultCollection); + List expected = Arrays.asList("1", "2"); + final List actual = new ArrayList<>(); + handler.handle(new SearchResultHandler.RecordProcessor() { + @Override + public boolean process(SearchResult record) throws NamingException { + actual.add(record.getNameInNamespace()); + return actual.size() < 2; + } + }); + assertEquals(expected, actual); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testGetAllLdapNamesNoRecords() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addEmptySearchResult(); + handler = new SearchResultHandler(resultCollection); + List actual = handler.getAllLdapNames(); + assertEquals("Resultset size", 0, actual.size()); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testGetAllLdapNamesWithExceptionInNamingEnumerationClose() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResultWithDns("1") + .addSearchResultWithDns("2"); + doThrow(NamingException.class).when(resultCollection.iterator().next()).close(); + handler = new SearchResultHandler(resultCollection); + List actual = handler.getAllLdapNames(); + assertEquals("Resultset size", 2, actual.size()); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testGetAllLdapNames() throws NamingException { + String objectDn1 = "cn=a1,dc=b,dc=c"; + String objectDn2 = "cn=a2,dc=b,dc=c"; + String objectDn3 = "cn=a3,dc=b,dc=c"; + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResultWithDns(objectDn1) + .addSearchResultWithDns(objectDn2, objectDn3); + handler = new SearchResultHandler(resultCollection); + List expected = Arrays.asList(objectDn1, objectDn2, objectDn3); + Collections.sort(expected); + List actual = handler.getAllLdapNames(); + Collections.sort(actual); + assertEquals(expected, actual); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testGetAllLdapNamesAndAttributes() throws NamingException { + SearchResult searchResult1 = mockSearchResult("cn=a1,dc=b,dc=c", + mockAttributes("attr1", "attr1value1")); + SearchResult searchResult2 = mockSearchResult("cn=a2,dc=b,dc=c", + mockAttributes("attr1", "attr1value2", "attr2", "attr2value1")); + SearchResult searchResult3 = mockSearchResult("cn=a3,dc=b,dc=c", + mockEmptyAttributes()); + + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResults(searchResult1) + .addSearchResults(searchResult2, searchResult3); + + handler = new SearchResultHandler(resultCollection); + List expected = Arrays.asList( + "cn=a1,dc=b,dc=c", "attr1value1", + "cn=a2,dc=b,dc=c", "attr1value2", "attr2value1", + "cn=a3,dc=b,dc=c"); + Collections.sort(expected); + List actual = handler.getAllLdapNamesAndAttributes(); + Collections.sort(actual); + assertEquals(expected, actual); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testHasSingleResultNoRecords() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addEmptySearchResult(); + handler = new SearchResultHandler(resultCollection); + assertFalse(handler.hasSingleResult()); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testHasSingleResult() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResultWithDns("1"); + handler = new SearchResultHandler(resultCollection); + assertTrue(handler.hasSingleResult()); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test + public void testHasSingleResultManyRecords() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addSearchResultWithDns("1") + .addSearchResultWithDns("2"); + handler = new SearchResultHandler(resultCollection); + assertFalse(handler.hasSingleResult()); + assertAllNamingEnumerationsClosed(resultCollection); + } + + @Test(expected = NamingException.class) + public void testGetSingleLdapNameNoRecords() throws NamingException { + MockResultCollection resultCollection = MockResultCollection.create() + .addEmptySearchResult(); + handler = new SearchResultHandler(resultCollection); + try { + handler.getSingleLdapName(); + } finally { + assertAllNamingEnumerationsClosed(resultCollection); + } + } + + @Test + public void testGetSingleLdapName() throws NamingException { + String objectDn = "cn=a,dc=b,dc=c"; + MockResultCollection resultCollection = MockResultCollection.create() + .addEmptySearchResult() + .addSearchResultWithDns(objectDn); + + handler = new SearchResultHandler(resultCollection); + String expected = objectDn; + String actual = handler.getSingleLdapName(); + assertEquals(expected, actual); + assertAllNamingEnumerationsClosed(resultCollection); + } + + private void assertAllNamingEnumerationsClosed(MockResultCollection resultCollection) throws NamingException { + for (NamingEnumeration namingEnumeration : resultCollection) { + verify(namingEnumeration, atLeastOnce()).close(); + } + } + + private static final class MockResultCollection extends AbstractCollection> { + + List> results = new ArrayList<>(); + + static MockResultCollection create() { + return new MockResultCollection(); + } + + MockResultCollection addSearchResultWithDns(String... dns) throws NamingException { + results.add(mockNamingEnumeration(dns)); + return this; + } + + MockResultCollection addSearchResults(SearchResult... dns) throws NamingException { + results.add(mockNamingEnumeration(dns)); + return this; + } + + MockResultCollection addEmptySearchResult() throws NamingException { + addSearchResults(); + return this; + } + + @Override + public Iterator> iterator() { + return results.iterator(); + } + + @Override + public int size() { + return results.size(); + } + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestUserFilter.java service/src/test/org/apache/hive/service/auth/ldap/TestUserFilter.java new file mode 100644 index 0000000..f941c9c --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestUserFilter.java @@ -0,0 +1,75 @@ +/** + * 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.hive.service.auth.ldap; + +import java.io.IOException; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import org.junit.Before; +import org.mockito.Mock; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestUserFilter { + + private FilterFactory factory; + private HiveConf conf; + + @Mock + private DirSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + factory = new UserFilterFactory(); + } + + @Test + public void testFactory() { + conf.unset(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER.varname); + assertNull(factory.getInstance(conf)); + + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "User1"); + assertNotNull(factory.getInstance(conf)); + } + + @Test + public void testApplyPositive() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "User1,User2,uSeR3"); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "User1"); + filter.apply(search, "uid=user2,ou=People,dc=example,dc=com"); + filter.apply(search, "User3@mydomain.com"); + } + + @Test(expected = AuthenticationException.class) + public void testApplyNegative() throws AuthenticationException, NamingException, IOException { + conf.setVar(HiveConf.ConfVars.HIVE_SERVER2_PLAIN_LDAP_USERFILTER, "User1,User2"); + + Filter filter = factory.getInstance(conf); + filter.apply(search, "User3"); + } +} diff --git service/src/test/org/apache/hive/service/auth/ldap/TestUserSearchFilter.java service/src/test/org/apache/hive/service/auth/ldap/TestUserSearchFilter.java new file mode 100644 index 0000000..0ba8371 --- /dev/null +++ service/src/test/org/apache/hive/service/auth/ldap/TestUserSearchFilter.java @@ -0,0 +1,79 @@ +/** + * 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.hive.service.auth.ldap; + +import java.io.IOException; +import javax.naming.NamingException; +import javax.security.sasl.AuthenticationException; +import org.apache.hadoop.hive.conf.HiveConf; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class TestUserSearchFilter { + + private FilterFactory factory; + private HiveConf conf; + + @Mock + private DirSearch search; + + @Before + public void setup() { + conf = new HiveConf(); + factory = new UserSearchFilterFactory(); + } + + @Test + public void testFactory() { + assertNotNull(factory.getInstance(conf)); + } + + @Test + public void testApplyPositive() throws AuthenticationException, NamingException, IOException { + Filter filter = factory.getInstance(conf); + + when(search.findUserDn(anyString())).thenReturn("cn=User1,ou=People,dc=example,dc=com"); + + filter.apply(search, "User1"); + } + + @Test(expected = AuthenticationException.class) + public void testApplyWhenNamingException() throws AuthenticationException, NamingException, IOException { + Filter filter = factory.getInstance(conf); + + when(search.findUserDn(anyString())).thenThrow(NamingException.class); + + filter.apply(search, "User3"); + } + + @Test(expected = AuthenticationException.class) + public void testApplyWhenNotFound() throws AuthenticationException, NamingException, IOException { + Filter filter = factory.getInstance(conf); + + when(search.findUserDn(anyString())).thenReturn(null); + + filter.apply(search, "User3"); + } +}