blob: 17934add81413187877e8a88929a88beea663278 [file] [log] [blame]
/**
* 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.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.HashSet;
import java.util.Collection;
import java.util.Set;
import javax.naming.AuthenticationException;
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 javax.naming.spi.InitialContextFactory;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.apache.hadoop.thirdparty.com.google.common.collect.Iterators;
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(s)
*/
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 aliases to bind to the LDAP server with. Each alias will have
* to have its username and password configured, see core-default.xml
* and GroupsMapping.md for details.
*/
public static final String BIND_USERS_KEY = LDAP_CONFIG_PREFIX +
".bind.users";
/*
* User to bind to the LDAP server with
*/
public static final String BIND_USER_SUFFIX = ".bind.user";
public static final String BIND_USER_KEY = LDAP_CONFIG_PREFIX +
BIND_USER_SUFFIX;
public static final String BIND_USER_DEFAULT = "";
/*
* Password for the bind user
*/
public static final String BIND_PASSWORD_SUFFIX = ".bind.password";
public static final String BIND_PASSWORD_KEY = LDAP_CONFIG_PREFIX +
BIND_PASSWORD_SUFFIX;
public static final String BIND_PASSWORD_DEFAULT = "";
public static final String BIND_PASSWORD_FILE_SUFFIX =
BIND_PASSWORD_SUFFIX + ".file";
public static final String BIND_PASSWORD_FILE_KEY = LDAP_CONFIG_PREFIX +
BIND_PASSWORD_FILE_SUFFIX;
public static final String BIND_PASSWORD_FILE_DEFAULT = "";
public static final String BIND_PASSWORD_ALIAS_SUFFIX =
BIND_PASSWORD_SUFFIX + ".alias";
public static final String BIND_PASSWORD_ALIAS_KEY =
LDAP_CONFIG_PREFIX + BIND_PASSWORD_ALIAS_SUFFIX;
public static final String BIND_PASSWORD_ALIAS_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
public static final String LDAP_NUM_ATTEMPTS_KEY =
LDAP_CONFIG_PREFIX + ".num.attempts";
public static final int LDAP_NUM_ATTEMPTS_DEFAULT = 3;
public static final String LDAP_NUM_ATTEMPTS_BEFORE_FAILOVER_KEY =
LDAP_CONFIG_PREFIX + ".num.attempts.before.failover";
public static final int LDAP_NUM_ATTEMPTS_BEFORE_FAILOVER_DEFAULT =
LDAP_NUM_ATTEMPTS_DEFAULT;
public static final String LDAP_CTX_FACTORY_CLASS_KEY =
LDAP_CONFIG_PREFIX + ".ctx.factory.class";
public static final String LDAP_CTX_FACTORY_CLASS_DEFAULT =
"com.sun.jndi.ldap.LdapCtxFactory";
/**
* The env key used for specifying a custom socket factory to be used for
* creating connections to the LDAP server. This is not a Hadoop conf key.
*/
private static final String LDAP_SOCKET_FACTORY_ENV_KEY =
"java.naming.ldap.factory.socket";
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 volatile Configuration conf;
private volatile Iterator<String> ldapUrls;
private String currentLdapUrl;
private volatile boolean useSsl;
private String keystore;
private String keystorePass;
private String truststore;
private String truststorePass;
/*
* Users to bind to when connecting to LDAP. This will be a rotating
* iterator, cycling back to the first user if necessary.
*/
private Iterator<BindUserInfo> bindUsers;
private BindUserInfo currentBindUser;
private volatile String userbaseDN;
private String groupbaseDN;
private String groupSearchFilter;
private volatile String userSearchFilter;
private volatile String memberOfAttr;
private String groupMemberAttr;
private volatile String groupNameAttr;
private volatile int groupHierarchyLevels;
private volatile String posixUidAttr;
private volatile String posixGidAttr;
private boolean isPosix;
private volatile boolean useOneQuery;
private int numAttempts;
private volatile int numAttemptsBeforeFailover;
private volatile String ldapCtxFactoryClassName;
/**
* 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<String> getGroups(String user) {
return new ArrayList<>(getGroupsSet(user));
}
/**
* 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<Rdn> 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<SearchResult> 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 Set<String> lookupGroup(SearchResult result, DirContext c,
int goUpHierarchy)
throws NamingException {
Set<String> groups = new LinkedHashSet<>();
Set<String> groupDNs = new HashSet<>();
NamingEnumeration<SearchResult> groupResults;
// 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) {
goUpGroupHierarchy(groupDNs, goUpHierarchy, groups);
}
}
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
*/
Set<String> 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<SearchResult> results = c.search(userbaseDN,
userSearchFilter, new Object[]{user}, SEARCH_CONTROLS);
// return empty list if the user can not be found.
if (!results.hasMoreElements()) {
LOG.debug("doGetGroups({}) returned no groups because the " +
"user is not found.", user);
return Collections.emptySet();
}
SearchResult result = results.nextElement();
Set<String> groups = Collections.emptySet();
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 LinkedHashSet<>();
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.isEmpty() || goUpHierarchy > 0) {
groups = lookupGroup(result, c, goUpHierarchy);
}
LOG.debug("doGetGroups({}) returned {}", user, groups);
return groups;
}
/* Helper function to get group name from search results.
*/
void getGroupNames(SearchResult groupResult, Collection<String> groups,
Collection<String> 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<String> groupDNs,
int goUpHierarchy,
Set<String> groups)
throws NamingException {
if (goUpHierarchy <= 0 || groups.isEmpty()) {
return;
}
DirContext context = getDirContext();
Set<String> 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<SearchResult> 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);
}
/**
* Check whether we should fail over to the next LDAP server.
* @param attemptsMadeWithSameLdap current number of attempts made
* with using same LDAP instance
* @param maxAttemptsBeforeFailover maximum number of attempts
* before failing over
* @return true if we should fail over to the next LDAP server
*/
protected boolean failover(
int attemptsMadeWithSameLdap, int maxAttemptsBeforeFailover) {
if (attemptsMadeWithSameLdap >= maxAttemptsBeforeFailover) {
String previousLdapUrl = currentLdapUrl;
currentLdapUrl = ldapUrls.next();
LOG.info("Reached {} attempts on {}, failing over to {}",
attemptsMadeWithSameLdap, previousLdapUrl, currentLdapUrl);
return true;
}
return false;
}
/**
* Switch to the next available user to bind to.
* @param e AuthenticationException encountered when contacting LDAP
*/
protected void switchBindUser(AuthenticationException e) {
BindUserInfo oldBindUser = this.currentBindUser;
currentBindUser = this.bindUsers.next();
if (!oldBindUser.equals(currentBindUser)) {
LOG.info("Switched from {} to {} after an AuthenticationException: {}",
oldBindUser, currentBindUser, e.getMessage());
}
}
private DirContext getDirContext() throws NamingException {
if (ctx == null) {
// Set up the initial environment for LDAP connectivity
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, ldapCtxFactoryClassName);
env.put(Context.PROVIDER_URL, currentLdapUrl);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
// Set up SSL security, if necessary
if (useSsl) {
env.put(Context.SECURITY_PROTOCOL, "ssl");
// It is necessary to use a custom socket factory rather than setting
// system properties to configure these options to avoid interfering
// with other SSL factories throughout the system
LdapSslSocketFactory.setConfigurations(keystore, keystorePass,
truststore, truststorePass);
env.put("java.naming.ldap.factory.socket",
LdapSslSocketFactory.class.getName());
}
env.put(Context.SECURITY_PRINCIPAL, currentBindUser.username);
env.put(Context.SECURITY_CREDENTIALS, currentBindUser.password);
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)));
// See HADOOP-17675 for details TLDR:
// From a native thread the thread's context classloader is null.
// jndi internally in the InitialDirContext specifies the context
// classloader for Class.forName, and as it is null, jndi will use the
// bootstrap classloader in this case to laod the socket factory
// implementation.
// BUT
// Bootstrap classloader does not have it in its classpath, so throws a
// ClassNotFoundException.
// This affects Impala for example when it uses LdapGroupsMapping.
ClassLoader currentContextLoader =
Thread.currentThread().getContextClassLoader();
if (currentContextLoader == null) {
try {
Thread.currentThread().setContextClassLoader(
this.getClass().getClassLoader());
ctx = new InitialDirContext(env);
} finally {
Thread.currentThread().setContextClassLoader(null);
}
} else {
ctx = new InitialDirContext(env);
}
}
return ctx;
}
/**
* Caches groups, no need to do that for this provider
*/
@Override
public void cacheGroupsRefresh() {
// 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<String> groups) {
// does nothing in this provider of user to groups mapping
}
@Override
public Set<String> getGroupsSet(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.
*/
// Tracks the number of attempts made using the same LDAP server
int atemptsBeforeFailover = 1;
for (int attempt = 1; attempt <= numAttempts; attempt++,
atemptsBeforeFailover++) {
try {
return doGetGroups(user, groupHierarchyLevels);
} catch (AuthenticationException e) {
switchBindUser(e);
} catch (NamingException e) {
LOG.warn("Failed to get groups for user {} (attempt={}/{}) using {}. " +
"Exception: ", user, attempt, numAttempts, currentLdapUrl, e);
LOG.trace("TRACE", e);
if (failover(atemptsBeforeFailover, numAttemptsBeforeFailover)) {
atemptsBeforeFailover = 0;
}
}
// Reset ctx so that new DirContext can be created with new connection
this.ctx = null;
}
return Collections.emptySet();
}
@Override
public synchronized Configuration getConf() {
return conf;
}
@Override
public synchronized void setConf(Configuration conf) {
this.conf = conf;
String[] urls = conf.getStrings(LDAP_URL_KEY, LDAP_URL_DEFAULT);
if (urls == null || urls.length == 0) {
throw new RuntimeException("LDAP URL(s) are not configured");
}
ldapUrls = Iterators.cycle(urls);
currentLdapUrl = ldapUrls.next();
useSsl = conf.getBoolean(LDAP_USE_SSL_KEY, LDAP_USE_SSL_DEFAULT);
if (useSsl) {
loadSslConf(conf);
}
initializeBindUsers();
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);
LOG.debug("Usersearch baseDN: {}", userbaseDN);
// Group search base which defaults to base dn.
groupbaseDN = conf.getTrimmed(GROUP_BASE_DN_KEY, baseDN);
LOG.debug("Groupsearch baseDN: {}", groupbaseDN);
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);
// LDAP_CTX_FACTORY_CLASS_DEFAULT is not open to unnamed modules
// in Java 11+, so the default value is set to null to avoid
// creating the instance for now.
Class<? extends InitialContextFactory> ldapCtxFactoryClass =
conf.getClass(LDAP_CTX_FACTORY_CLASS_KEY, null,
InitialContextFactory.class);
if (ldapCtxFactoryClass != null) {
ldapCtxFactoryClassName = ldapCtxFactoryClass.getName();
} else {
// The default value is set afterwards.
ldapCtxFactoryClassName = LDAP_CTX_FACTORY_CLASS_DEFAULT;
}
this.numAttempts = conf.getInt(LDAP_NUM_ATTEMPTS_KEY,
LDAP_NUM_ATTEMPTS_DEFAULT);
this.numAttemptsBeforeFailover = conf.getInt(
LDAP_NUM_ATTEMPTS_BEFORE_FAILOVER_KEY,
LDAP_NUM_ATTEMPTS_BEFORE_FAILOVER_DEFAULT);
}
/**
* Get URLs of configured LDAP servers.
* @return URLs of LDAP servers being used.
*/
public Iterator<String> getLdapUrls() {
return ldapUrls;
}
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 config, String alias, String defaultPass) {
String password = defaultPass;
try {
char[] passchars = config.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(
Files.newInputStream(Paths.get(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);
}
}
private void initializeBindUsers() {
List<BindUserInfo> bindUsersConfigured = new ArrayList<>();
String[] bindUserAliases = conf.getStrings(BIND_USERS_KEY);
if (bindUserAliases != null && bindUserAliases.length > 0) {
for (String bindUserAlias : bindUserAliases) {
String userConfPrefix = BIND_USERS_KEY + "." + bindUserAlias;
String bindUsername = conf.get(userConfPrefix + BIND_USER_SUFFIX);
String bindPassword = getPasswordForBindUser(userConfPrefix);
if (bindUsername == null || bindPassword == null) {
throw new RuntimeException("Bind username or password not " +
"configured for user: " + bindUserAlias);
}
bindUsersConfigured.add(new BindUserInfo(bindUsername, bindPassword));
}
} else {
String bindUsername = conf.get(BIND_USER_KEY, BIND_USER_DEFAULT);
String bindPassword = getPasswordForBindUser(LDAP_CONFIG_PREFIX);
bindUsersConfigured.add(new BindUserInfo(bindUsername, bindPassword));
}
this.bindUsers = Iterators.cycle(bindUsersConfigured);
this.currentBindUser = this.bindUsers.next();
}
private String getPasswordForBindUser(String keyPrefix) {
String password;
String alias = conf.get(keyPrefix + BIND_PASSWORD_ALIAS_SUFFIX,
BIND_PASSWORD_ALIAS_DEFAULT);
password = getPasswordFromCredentialProviders(conf, alias, "");
if (password.isEmpty()) {
password = getPassword(conf, keyPrefix + BIND_PASSWORD_SUFFIX,
BIND_PASSWORD_DEFAULT);
if (password.isEmpty()) {
password = extractPassword(conf.get(
keyPrefix + BIND_PASSWORD_FILE_SUFFIX, BIND_PASSWORD_FILE_DEFAULT));
}
}
return password;
}
private final static class BindUserInfo {
private final String username;
private final String password;
private BindUserInfo(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof BindUserInfo)) {
return false;
}
return this.username.equals(((BindUserInfo) o).username);
}
@Override
public int hashCode() {
return this.username.hashCode();
}
@Override
public String toString() {
return this.username;
}
}
/**
* An private internal socket factory used to create SSL sockets with custom
* configuration. There is no way to pass a specific instance of a factory to
* the Java naming services, and the instantiated socket factory is not
* passed any contextual information, so all information must be encapsulated
* directly in the class. Static fields are used here to achieve this. This is
* safe since the only usage of {@link LdapGroupsMapping} is within
* {@link Groups}, which is a singleton (see the GROUPS field).
* <p>
* This has nearly the same behavior as an {@link SSLSocketFactory}. The only
* additional logic is to configure the key store and trust store.
* <p>
* This is public only to be accessible by the Java naming services.
*/
@InterfaceAudience.Private
public static class LdapSslSocketFactory extends SocketFactory {
/** Cached value lazy-loaded by {@link #getDefault()}. */
private static LdapSslSocketFactory defaultSslFactory;
private static String keyStoreLocation;
private static String keyStorePassword;
private static String trustStoreLocation;
private static String trustStorePassword;
private final SSLSocketFactory socketFactory;
LdapSslSocketFactory(SSLSocketFactory wrappedSocketFactory) {
this.socketFactory = wrappedSocketFactory;
}
public static synchronized SocketFactory getDefault() {
if (defaultSslFactory == null) {
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(createKeyManagers(), createTrustManagers(), null);
defaultSslFactory =
new LdapSslSocketFactory(context.getSocketFactory());
LOG.info("Successfully instantiated LdapSslSocketFactory with "
+ "keyStoreLocation = {} and trustStoreLocation = {}",
keyStoreLocation, trustStoreLocation);
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException("Unable to create SSLSocketFactory", e);
}
}
return defaultSslFactory;
}
static synchronized void setConfigurations(String newKeyStoreLocation,
String newKeyStorePassword, String newTrustStoreLocation,
String newTrustStorePassword) {
LdapSslSocketFactory.keyStoreLocation = newKeyStoreLocation;
LdapSslSocketFactory.keyStorePassword = newKeyStorePassword;
LdapSslSocketFactory.trustStoreLocation = newTrustStoreLocation;
LdapSslSocketFactory.trustStorePassword = newTrustStorePassword;
}
private static KeyManager[] createKeyManagers()
throws IOException, GeneralSecurityException {
if (keyStoreLocation.isEmpty()) {
return null;
}
KeyManagerFactory keyMgrFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyMgrFactory.init(createKeyStore(keyStoreLocation, keyStorePassword),
getPasswordCharArray(keyStorePassword));
return keyMgrFactory.getKeyManagers();
}
private static TrustManager[] createTrustManagers()
throws IOException, GeneralSecurityException {
if (trustStoreLocation.isEmpty()) {
return null;
}
TrustManagerFactory trustMgrFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustMgrFactory.init(
createKeyStore(trustStoreLocation, trustStorePassword));
return trustMgrFactory.getTrustManagers();
}
private static KeyStore createKeyStore(String location, String password)
throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream keyStoreInput = new FileInputStream(location)) {
keyStore.load(keyStoreInput, getPasswordCharArray(password));
}
return keyStore;
}
private static char[] getPasswordCharArray(String password) {
if (password == null || password.isEmpty()) {
return null;
}
return password.toCharArray();
}
@Override
public Socket createSocket() throws IOException {
return socketFactory.createSocket();
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return socketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost,
int localPort) throws IOException {
return socketFactory.createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return socketFactory.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress address, int port,
InetAddress localAddress, int localPort) throws IOException {
return socketFactory.createSocket(address, port, localAddress, localPort);
}
}
}