diff --git a/pom.xml b/pom.xml index fcbe0ba..22c3500 100644 --- a/pom.xml +++ b/pom.xml @@ -640,6 +640,8 @@ org/apache/hadoop/util/LineReader.class org/apache/hadoop/crypto/key/kms/KMSClientProvider.class org/apache/hadoop/crypto/key/kms/KMSClientProvider$*.class + org/apache/hadoop/security/LdapGroupsMapping.class + org/apache/hadoop/security/LdapGroupsMapping$*.class diff --git a/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java b/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java new file mode 100644 index 0000000..4c7370f --- /dev/null +++ b/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java @@ -0,0 +1,749 @@ +/** + * 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.hadoop.security; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.HashSet; +import java.util.Collection; +import java.util.Set; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.apache.hadoop.classification.InterfaceAudience; +import org.apache.hadoop.classification.InterfaceStability; +import org.apache.hadoop.conf.Configurable; +import org.apache.hadoop.conf.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An implementation of {@link GroupMappingServiceProvider} which + * connects directly to an LDAP server for determining group membership. + * + * This provider should be used only if it is necessary to map users to + * groups that reside exclusively in an Active Directory or LDAP installation. + * The common case for a Hadoop installation will be that LDAP users and groups + * materialized on the Unix servers, and for an installation like that, + * ShellBasedUnixGroupsMapping is preferred. However, in cases where + * those users and groups aren't materialized in Unix, but need to be used for + * access control, this class may be used to communicate directly with the LDAP + * server. + * + * It is important to note that resolving group mappings will incur network + * traffic, and may cause degraded performance, although user-group mappings + * will be cached via the infrastructure provided by {@link Groups}. + * + * This implementation does not support configurable search limits. If a filter + * is used for searching users or groups which returns more results than are + * allowed by the server, an exception will be thrown. + * + * The implementation attempts to resolve group hierarchies, + * to a configurable limit. + * If the limit is 0, in order to be considered a member of a group, + * the user must be an explicit member in LDAP. Otherwise, it will traverse the + * group hierarchy n levels up. + */ +@InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"}) +@InterfaceStability.Evolving +public class LdapGroupsMapping + implements GroupMappingServiceProvider, Configurable { + + public static final String LDAP_CONFIG_PREFIX = "hadoop.security.group.mapping.ldap"; + + /* + * URL of the LDAP server + */ + public static final String LDAP_URL_KEY = LDAP_CONFIG_PREFIX + ".url"; + public static final String LDAP_URL_DEFAULT = ""; + + /* + * Should SSL be used to connect to the server + */ + public static final String LDAP_USE_SSL_KEY = LDAP_CONFIG_PREFIX + ".ssl"; + public static final Boolean LDAP_USE_SSL_DEFAULT = false; + + /* + * File path to the location of the SSL keystore to use + */ + public static final String LDAP_KEYSTORE_KEY = LDAP_CONFIG_PREFIX + ".ssl.keystore"; + public static final String LDAP_KEYSTORE_DEFAULT = ""; + + /* + * Password for the keystore + */ + public static final String LDAP_KEYSTORE_PASSWORD_KEY = LDAP_CONFIG_PREFIX + ".ssl.keystore.password"; + public static final String LDAP_KEYSTORE_PASSWORD_DEFAULT = ""; + + public static final String LDAP_KEYSTORE_PASSWORD_FILE_KEY = LDAP_KEYSTORE_PASSWORD_KEY + ".file"; + public static final String LDAP_KEYSTORE_PASSWORD_FILE_DEFAULT = ""; + + + /** + * File path to the location of the SSL truststore to use + */ + public static final String LDAP_TRUSTSTORE_KEY = LDAP_CONFIG_PREFIX + + ".ssl.truststore"; + + /** + * The key of the credential entry containing the password for + * the LDAP SSL truststore + */ + public static final String LDAP_TRUSTSTORE_PASSWORD_KEY = + LDAP_CONFIG_PREFIX +".ssl.truststore.password"; + + /** + * The path to a file containing the password for + * the LDAP SSL truststore + */ + public static final String LDAP_TRUSTSTORE_PASSWORD_FILE_KEY = + LDAP_TRUSTSTORE_PASSWORD_KEY + ".file"; + + /* + * User to bind to the LDAP server with + */ + public static final String BIND_USER_KEY = LDAP_CONFIG_PREFIX + ".bind.user"; + public static final String BIND_USER_DEFAULT = ""; + + /* + * Password for the bind user + */ + public static final String BIND_PASSWORD_KEY = LDAP_CONFIG_PREFIX + ".bind.password"; + public static final String BIND_PASSWORD_DEFAULT = ""; + + public static final String BIND_PASSWORD_FILE_KEY = BIND_PASSWORD_KEY + ".file"; + public static final String BIND_PASSWORD_FILE_DEFAULT = ""; + + /* + * Base distinguished name to use for searches + */ + public static final String BASE_DN_KEY = LDAP_CONFIG_PREFIX + ".base"; + public static final String BASE_DN_DEFAULT = ""; + + /* + * Base DN used in user search. + */ + public static final String USER_BASE_DN_KEY = + LDAP_CONFIG_PREFIX + ".userbase"; + + /* + * Base DN used in group search. + */ + public static final String GROUP_BASE_DN_KEY = + LDAP_CONFIG_PREFIX + ".groupbase"; + + + /* + * Any additional filters to apply when searching for users + */ + public static final String USER_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.user"; + public static final String USER_SEARCH_FILTER_DEFAULT = "(&(objectClass=user)(sAMAccountName={0}))"; + + /* + * Any additional filters to apply when finding relevant groups + */ + public static final String GROUP_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.group"; + public static final String GROUP_SEARCH_FILTER_DEFAULT = "(objectClass=group)"; + + /* + * LDAP attribute to use for determining group membership + */ + public static final String MEMBEROF_ATTR_KEY = + LDAP_CONFIG_PREFIX + ".search.attr.memberof"; + public static final String MEMBEROF_ATTR_DEFAULT = ""; + + /* + * LDAP attribute to use for determining group membership + */ + public static final String GROUP_MEMBERSHIP_ATTR_KEY = LDAP_CONFIG_PREFIX + ".search.attr.member"; + public static final String GROUP_MEMBERSHIP_ATTR_DEFAULT = "member"; + + /* + * LDAP attribute to use for identifying a group's name + */ + public static final String GROUP_NAME_ATTR_KEY = LDAP_CONFIG_PREFIX + ".search.attr.group.name"; + public static final String GROUP_NAME_ATTR_DEFAULT = "cn"; + + /* + * How many levels to traverse when checking for groups in the org hierarchy + */ + public static final String GROUP_HIERARCHY_LEVELS_KEY = + LDAP_CONFIG_PREFIX + ".search.group.hierarchy.levels"; + public static final int GROUP_HIERARCHY_LEVELS_DEFAULT = 0; + + /* + * LDAP attribute names to use when doing posix-like lookups + */ + public static final String POSIX_UID_ATTR_KEY = LDAP_CONFIG_PREFIX + ".posix.attr.uid.name"; + public static final String POSIX_UID_ATTR_DEFAULT = "uidNumber"; + + public static final String POSIX_GID_ATTR_KEY = LDAP_CONFIG_PREFIX + ".posix.attr.gid.name"; + public static final String POSIX_GID_ATTR_DEFAULT = "gidNumber"; + + /* + * Posix attributes + */ + public static final String POSIX_GROUP = "posixGroup"; + public static final String POSIX_ACCOUNT = "posixAccount"; + + /* + * LDAP {@link SearchControls} attribute to set the time limit + * for an invoked directory search. Prevents infinite wait cases. + */ + public static final String DIRECTORY_SEARCH_TIMEOUT = + LDAP_CONFIG_PREFIX + ".directory.search.timeout"; + public static final int DIRECTORY_SEARCH_TIMEOUT_DEFAULT = 10000; // 10s + + public static final String CONNECTION_TIMEOUT = + LDAP_CONFIG_PREFIX + ".connection.timeout.ms"; + public static final int CONNECTION_TIMEOUT_DEFAULT = 60 * 1000; // 60 seconds + public static final String READ_TIMEOUT = + LDAP_CONFIG_PREFIX + ".read.timeout.ms"; + public static final int READ_TIMEOUT_DEFAULT = 60 * 1000; // 60 seconds + + private static final Logger LOG = + LoggerFactory.getLogger(LdapGroupsMapping.class); + + static final SearchControls SEARCH_CONTROLS = new SearchControls(); + static { + SEARCH_CONTROLS.setSearchScope(SearchControls.SUBTREE_SCOPE); + } + + private DirContext ctx; + private Configuration conf; + + private String ldapUrl; + private boolean useSsl; + private String keystore; + private String keystorePass; + private String truststore; + private String truststorePass; + private String bindUser; + private String bindPassword; + private String userbaseDN; + private String groupbaseDN; + private String groupSearchFilter; + private String userSearchFilter; + private String memberOfAttr; + private String groupMemberAttr; + private String groupNameAttr; + private int groupHierarchyLevels; + private String posixUidAttr; + private String posixGidAttr; + private boolean isPosix; + private boolean useOneQuery; + + public static final int RECONNECT_RETRY_COUNT = 3; + + /** + * Returns list of groups for a user. + * + * The LdapCtx which underlies the DirContext object is not thread-safe, so + * we need to block around this whole method. The caching infrastructure will + * ensure that performance stays in an acceptable range. + * + * @param user get groups for this user + * @return list of groups for a given user + */ + @Override + public synchronized List getGroups(String user) { + /* + * Normal garbage collection takes care of removing Context instances when they are no longer in use. + * Connections used by Context instances being garbage collected will be closed automatically. + * So in case connection is closed and gets CommunicationException, retry some times with new new DirContext/connection. + */ + for(int retry = 0; retry < RECONNECT_RETRY_COUNT; retry++) { + try { + return doGetGroups(user, groupHierarchyLevels); + } catch (NamingException e) { + LOG.warn("Failed to get groups for user " + user + " (retry=" + retry + + ") by " + e); + LOG.trace("TRACE", e); + } + + //reset ctx so that new DirContext can be created with new connection + this.ctx = null; + } + + return Collections.emptyList(); + } + + /** + * A helper method to get the Relative Distinguished Name (RDN) from + * Distinguished name (DN). According to Active Directory documentation, + * a group object's RDN is a CN. + * + * @param distinguishedName A string representing a distinguished name. + * @throws NamingException if the DN is malformed. + * @return a string which represents the RDN + */ + private String getRelativeDistinguishedName(String distinguishedName) + throws NamingException { + LdapName ldn = new LdapName(distinguishedName); + List rdns = ldn.getRdns(); + if (rdns.isEmpty()) { + throw new NamingException("DN is empty"); + } + Rdn rdn = rdns.get(rdns.size()-1); + if (rdn.getType().equalsIgnoreCase(groupNameAttr)) { + String groupName = (String)rdn.getValue(); + return groupName; + } + throw new NamingException("Unable to find RDN: The DN " + + distinguishedName + " is malformed."); + } + + /** + * Look up groups using posixGroups semantics. Use posix gid/uid to find + * groups of the user. + * + * @param result the result object returned from the prior user lookup. + * @param c the context object of the LDAP connection. + * @return an object representing the search result. + * + * @throws NamingException if the server does not support posixGroups + * semantics. + */ + private NamingEnumeration lookupPosixGroup(SearchResult result, + DirContext c) throws NamingException { + String gidNumber = null; + String uidNumber = null; + Attribute gidAttribute = result.getAttributes().get(posixGidAttr); + Attribute uidAttribute = result.getAttributes().get(posixUidAttr); + String reason = ""; + if (gidAttribute == null) { + reason = "Can't find attribute '" + posixGidAttr + "'."; + } else { + gidNumber = gidAttribute.get().toString(); + } + if (uidAttribute == null) { + reason = "Can't find attribute '" + posixUidAttr + "'."; + } else { + uidNumber = uidAttribute.get().toString(); + } + if (uidNumber != null && gidNumber != null) { + return c.search(groupbaseDN, + "(&"+ groupSearchFilter + "(|(" + posixGidAttr + "={0})" + + "(" + groupMemberAttr + "={1})))", + new Object[] {gidNumber, uidNumber}, + SEARCH_CONTROLS); + } + throw new NamingException("The server does not support posixGroups " + + "semantics. Reason: " + reason + + " Returned user object: " + result.toString()); + } + + /** + * Perform the second query to get the groups of the user. + * + * If posixGroups is enabled, use use posix gid/uid to find. + * Otherwise, use the general group member attribute to find it. + * + * @param result the result object returned from the prior user lookup. + * @param c the context object of the LDAP connection. + * @return a list of strings representing group names of the user. + * @throws NamingException if unable to find group names + */ + private List lookupGroup(SearchResult result, DirContext c, + int goUpHierarchy) + throws NamingException { + List groups = new ArrayList(); + Set groupDNs = new HashSet(); + + NamingEnumeration groupResults = null; + // perform the second LDAP query + if (isPosix) { + groupResults = lookupPosixGroup(result, c); + } else { + String userDn = result.getNameInNamespace(); + groupResults = + c.search(groupbaseDN, + "(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))", + new Object[]{userDn}, + SEARCH_CONTROLS); + } + // if the second query is successful, group objects of the user will be + // returned. Get group names from the returned objects. + if (groupResults != null) { + while (groupResults.hasMoreElements()) { + SearchResult groupResult = groupResults.nextElement(); + getGroupNames(groupResult, groups, groupDNs, goUpHierarchy > 0); + } + if (goUpHierarchy > 0 && !isPosix) { + // convert groups to a set to ensure uniqueness + Set groupset = new HashSet(groups); + goUpGroupHierarchy(groupDNs, goUpHierarchy, groupset); + // convert set back to list for compatibility + groups = new ArrayList(groupset); + } + } + return groups; + } + + /** + * Perform LDAP queries to get group names of a user. + * + * Perform the first LDAP query to get the user object using the user's name. + * If one-query is enabled, retrieve the group names from the user object. + * If one-query is disabled, or if it failed, perform the second query to + * get the groups. + * + * @param user user name + * @return a list of group names for the user. If the user can not be found, + * return an empty string array. + * @throws NamingException if unable to get group names + */ + List doGetGroups(String user, int goUpHierarchy) + throws NamingException { + DirContext c = getDirContext(); + + // Search for the user. We'll only ever need to look at the first result + NamingEnumeration results = c.search(userbaseDN, + userSearchFilter, new Object[]{user}, SEARCH_CONTROLS); + // return empty list if the user can not be found. + if (!results.hasMoreElements()) { + if (LOG.isDebugEnabled()) { + LOG.debug("doGetGroups(" + user + ") returned no groups because the " + + "user is not found."); + } + return new ArrayList(); + } + SearchResult result = results.nextElement(); + + List groups = null; + if (useOneQuery) { + try { + /** + * For Active Directory servers, the user object has an attribute + * 'memberOf' that represents the DNs of group objects to which the + * user belongs. So the second query may be skipped. + */ + Attribute groupDNAttr = result.getAttributes().get(memberOfAttr); + if (groupDNAttr == null) { + throw new NamingException("The user object does not have '" + + memberOfAttr + "' attribute." + + "Returned user object: " + result.toString()); + } + groups = new ArrayList(); + NamingEnumeration groupEnumeration = groupDNAttr.getAll(); + while (groupEnumeration.hasMore()) { + String groupDN = groupEnumeration.next().toString(); + groups.add(getRelativeDistinguishedName(groupDN)); + } + } catch (NamingException e) { + // If the first lookup failed, fall back to the typical scenario. + LOG.info("Failed to get groups from the first lookup. Initiating " + + "the second LDAP query using the user's DN.", e); + } + } + if (groups == null || groups.isEmpty() || goUpHierarchy > 0) { + groups = lookupGroup(result, c, goUpHierarchy); + } + if (LOG.isDebugEnabled()) { + LOG.debug("doGetGroups(" + user + ") returned " + groups); + } + return groups; + } + + /* Helper function to get group name from search results. + */ + void getGroupNames(SearchResult groupResult, Collection groups, + Collection groupDNs, boolean doGetDNs) + throws NamingException { + Attribute groupName = groupResult.getAttributes().get(groupNameAttr); + if (groupName == null) { + throw new NamingException("The group object does not have " + + "attribute '" + groupNameAttr + "'."); + } + groups.add(groupName.get().toString()); + if (doGetDNs) { + groupDNs.add(groupResult.getNameInNamespace()); + } + } + + /* Implementation for walking up the ldap hierarchy + * This function will iteratively find the super-group memebership of + * groups listed in groupDNs and add them to + * the groups set. It will walk up the hierarchy goUpHierarchy levels. + * Note: This is an expensive operation and settings higher than 1 + * are NOT recommended as they will impact both the speed and + * memory usage of all operations. + * The maximum time for this function will be bounded by the ldap query + * timeout and the number of ldap queries that it will make, which is + * max(Recur Depth in LDAP, goUpHierarcy) * DIRECTORY_SEARCH_TIMEOUT + * + * @param ctx - The context for contacting the ldap server + * @param groupDNs - the distinguished name of the groups whose parents we + * want to look up + * @param goUpHierarchy - the number of levels to go up, + * @param groups - Output variable to store all groups that will be added + */ + void goUpGroupHierarchy(Set groupDNs, + int goUpHierarchy, + Set groups) + throws NamingException { + if (goUpHierarchy <= 0 || groups.isEmpty()) { + return; + } + DirContext context = getDirContext(); + Set nextLevelGroups = new HashSet(); + StringBuilder filter = new StringBuilder(); + filter.append("(&").append(groupSearchFilter).append("(|"); + for (String dn : groupDNs) { + filter.append("(").append(groupMemberAttr).append("=") + .append(dn).append(")"); + } + filter.append("))"); + LOG.debug("Ldap group query string: " + filter.toString()); + NamingEnumeration groupResults = + context.search(groupbaseDN, + filter.toString(), + SEARCH_CONTROLS); + while (groupResults.hasMoreElements()) { + SearchResult groupResult = groupResults.nextElement(); + getGroupNames(groupResult, groups, nextLevelGroups, true); + } + goUpGroupHierarchy(nextLevelGroups, goUpHierarchy - 1, groups); + } + + DirContext getDirContext() throws NamingException { + if (ctx == null) { + // Set up the initial environment for LDAP connectivity + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, ldapUrl); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + + // Set up SSL security, if necessary + if (useSsl) { + env.put(Context.SECURITY_PROTOCOL, "ssl"); + if (!keystore.isEmpty()) { + System.setProperty("javax.net.ssl.keyStore", keystore); + } + if (!keystorePass.isEmpty()) { + System.setProperty("javax.net.ssl.keyStorePassword", keystorePass); + } + if (!truststore.isEmpty()) { + System.setProperty("javax.net.ssl.trustStore", truststore); + } + if (!truststorePass.isEmpty()) { + System.setProperty("javax.net.ssl.trustStorePassword", + truststorePass); + } + } + + env.put(Context.SECURITY_PRINCIPAL, bindUser); + env.put(Context.SECURITY_CREDENTIALS, bindPassword); + + env.put("com.sun.jndi.ldap.connect.timeout", conf.get(CONNECTION_TIMEOUT, + String.valueOf(CONNECTION_TIMEOUT_DEFAULT))); + env.put("com.sun.jndi.ldap.read.timeout", conf.get(READ_TIMEOUT, + String.valueOf(READ_TIMEOUT_DEFAULT))); + + ctx = new InitialDirContext(env); + } + return ctx; + } + + /** + * Caches groups, no need to do that for this provider + */ + @Override + public void cacheGroupsRefresh() throws IOException { + // does nothing in this provider of user to groups mapping + } + + /** + * Adds groups to cache, no need to do that for this provider + * + * @param groups unused + */ + @Override + public void cacheGroupsAdd(List groups) throws IOException { + // does nothing in this provider of user to groups mapping + } + + @Override + public synchronized Configuration getConf() { + return conf; + } + + @Override + public synchronized void setConf(Configuration conf) { + ldapUrl = conf.get(LDAP_URL_KEY, LDAP_URL_DEFAULT); + if (ldapUrl == null || ldapUrl.isEmpty()) { + throw new RuntimeException("LDAP URL is not configured"); + } + + useSsl = conf.getBoolean(LDAP_USE_SSL_KEY, LDAP_USE_SSL_DEFAULT); + if (useSsl) { + loadSslConf(conf); + } + + bindUser = conf.get(BIND_USER_KEY, BIND_USER_DEFAULT); + bindPassword = getPassword(conf, BIND_PASSWORD_KEY, BIND_PASSWORD_DEFAULT); + if (bindPassword.isEmpty()) { + bindPassword = extractPassword( + conf.get(BIND_PASSWORD_FILE_KEY, BIND_PASSWORD_FILE_DEFAULT)); + } + + String baseDN = conf.getTrimmed(BASE_DN_KEY, BASE_DN_DEFAULT); + + //User search base which defaults to base dn. + userbaseDN = conf.getTrimmed(USER_BASE_DN_KEY, baseDN); + if (LOG.isDebugEnabled()) { + LOG.debug("Usersearch baseDN: " + userbaseDN); + } + + //Group search base which defaults to base dn. + groupbaseDN = conf.getTrimmed(GROUP_BASE_DN_KEY, baseDN); + if (LOG.isDebugEnabled()) { + LOG.debug("Groupsearch baseDN: " + userbaseDN); + } + + groupSearchFilter = + conf.get(GROUP_SEARCH_FILTER_KEY, GROUP_SEARCH_FILTER_DEFAULT); + userSearchFilter = + conf.get(USER_SEARCH_FILTER_KEY, USER_SEARCH_FILTER_DEFAULT); + isPosix = groupSearchFilter.contains(POSIX_GROUP) && userSearchFilter + .contains(POSIX_ACCOUNT); + memberOfAttr = + conf.get(MEMBEROF_ATTR_KEY, MEMBEROF_ATTR_DEFAULT); + // if memberOf attribute is set, resolve group names from the attribute + // of user objects. + useOneQuery = !memberOfAttr.isEmpty(); + groupMemberAttr = + conf.get(GROUP_MEMBERSHIP_ATTR_KEY, GROUP_MEMBERSHIP_ATTR_DEFAULT); + groupNameAttr = + conf.get(GROUP_NAME_ATTR_KEY, GROUP_NAME_ATTR_DEFAULT); + groupHierarchyLevels = + conf.getInt(GROUP_HIERARCHY_LEVELS_KEY, GROUP_HIERARCHY_LEVELS_DEFAULT); + posixUidAttr = + conf.get(POSIX_UID_ATTR_KEY, POSIX_UID_ATTR_DEFAULT); + posixGidAttr = + conf.get(POSIX_GID_ATTR_KEY, POSIX_GID_ATTR_DEFAULT); + + int dirSearchTimeout = conf.getInt(DIRECTORY_SEARCH_TIMEOUT, DIRECTORY_SEARCH_TIMEOUT_DEFAULT); + SEARCH_CONTROLS.setTimeLimit(dirSearchTimeout); + // Limit the attributes returned to only those required to speed up the search. + // See HADOOP-10626 and HADOOP-12001 for more details. + String[] returningAttributes; + if (useOneQuery) { + returningAttributes = new String[] { + groupNameAttr, posixUidAttr, posixGidAttr, memberOfAttr}; + } else { + returningAttributes = new String[] { + groupNameAttr, posixUidAttr, posixGidAttr}; + } + SEARCH_CONTROLS.setReturningAttributes(returningAttributes); + + this.conf = conf; + } + + private void loadSslConf(Configuration sslConf) { + keystore = sslConf.get(LDAP_KEYSTORE_KEY, LDAP_KEYSTORE_DEFAULT); + keystorePass = getPassword(sslConf, LDAP_KEYSTORE_PASSWORD_KEY, + LDAP_KEYSTORE_PASSWORD_DEFAULT); + if (keystorePass.isEmpty()) { + keystorePass = extractPassword(sslConf.get( + LDAP_KEYSTORE_PASSWORD_FILE_KEY, + LDAP_KEYSTORE_PASSWORD_FILE_DEFAULT)); + } + + truststore = sslConf.get(LDAP_TRUSTSTORE_KEY, ""); + truststorePass = getPasswordFromCredentialProviders( + sslConf, LDAP_TRUSTSTORE_PASSWORD_KEY, ""); + if (truststorePass.isEmpty()) { + truststorePass = extractPassword( + sslConf.get(LDAP_TRUSTSTORE_PASSWORD_FILE_KEY, "")); + } + } + + String getPasswordFromCredentialProviders( + Configuration conf, String alias, String defaultPass) { + String password = defaultPass; + try { + char[] passchars = conf.getPasswordFromCredentialProviders(alias); + if (passchars != null) { + password = new String(passchars); + } + } catch (IOException ioe) { + LOG.warn("Exception while trying to get password for alias {}: {}", + alias, ioe); + } + return password; + } + + /** + * Passwords should not be stored in configuration. Use + * {@link #getPasswordFromCredentialProviders( + * Configuration, String, String)} + * to avoid reading passwords from a configuration file. + */ + @Deprecated + String getPassword(Configuration conf, String alias, String defaultPass) { + String password = defaultPass; + try { + char[] passchars = conf.getPassword(alias); + if (passchars != null) { + password = new String(passchars); + } + } catch (IOException ioe) { + LOG.warn("Exception while trying to get password for alias " + alias + + ": ", ioe); + } + return password; + } + + String extractPassword(String pwFile) { + if (pwFile.isEmpty()) { + // If there is no password file defined, we'll assume that we should do + // an anonymous bind + return ""; + } + + StringBuilder password = new StringBuilder(); + try (Reader reader = new InputStreamReader( + new FileInputStream(pwFile), StandardCharsets.UTF_8)) { + int c = reader.read(); + while (c > -1) { + password.append((char)c); + c = reader.read(); + } + return password.toString().trim(); + } catch (IOException ioe) { + throw new RuntimeException("Could not read password file: " + pwFile, ioe); + } + } +}