hadoop LdapGroupsMapping 源码

  • 2022-10-20
  • 浏览 (426)

haddop LdapGroupsMapping 代码

文件路径:/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java

/**
 * 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.Arrays;
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.classification.VisibleForTesting;
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";

  public static final String GROUP_SEARCH_FILTER_PATTERN =
      LDAP_CONFIG_PREFIX + ".group.search.filter.pattern";
  public static final String GROUP_SEARCH_FILTER_PATTERN_DEFAULT = "";

  /*
   * 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;
  private volatile String[] groupSearchFilterParams;

  /**
   * 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
   */
  @VisibleForTesting
  Set<String> lookupGroup(SearchResult result, DirContext c,
      int goUpHierarchy)
      throws NamingException {
    Set<String> groups = new LinkedHashSet<>();
    Set<String> groupDNs = new HashSet<>();

    NamingEnumeration<SearchResult> groupResults;

    String[] resolved = resolveCustomGroupFilterArgs(result);
    // If custom group filter argument is supplied, use that!!!
    if (resolved != null) {
      groupResults =
          c.search(groupbaseDN, groupSearchFilter, resolved, SEARCH_CONTROLS);
    } else if (isPosix) {
      // perform the second LDAP query
      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;
  }

  private String[] resolveCustomGroupFilterArgs(SearchResult result)
      throws NamingException {
    if (groupSearchFilterParams != null) {
      String[] filterElems = new String[groupSearchFilterParams.length];
      for (int i = 0; i < groupSearchFilterParams.length; i++) {
        // Specific handling for userDN.
        if (groupSearchFilterParams[i].equalsIgnoreCase("userDN")) {
          filterElems[i] = result.getNameInNamespace();
        } else {
          filterElems[i] =
              result.getAttributes().get(groupSearchFilterParams[i]).get()
                  .toString();
        }
      }
      return filterElems;
    }
    return null;
  }

  /**
   * 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.
        // In order to force the fallback, we need to reset groups collection.
        groups.clear();
        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);
    String groupSearchFilterParamCSV = conf.get(GROUP_SEARCH_FILTER_PATTERN,
        GROUP_SEARCH_FILTER_PATTERN_DEFAULT);
    if(groupSearchFilterParamCSV!=null && !groupSearchFilterParamCSV.isEmpty()) {
      LOG.debug("Using custom group search filters: {}", groupSearchFilterParamCSV);
      groupSearchFilterParams = groupSearchFilterParamCSV.split(",");
    }

    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};
    }

    // If custom group filter is being used, fetch attributes in the filter
    // as well.
    ArrayList<String> customAttributes = new ArrayList<>();
    if (groupSearchFilterParams != null) {
      customAttributes.addAll(Arrays.asList(groupSearchFilterParams));
    }
    customAttributes.addAll(Arrays.asList(returningAttributes));
    SEARCH_CONTROLS
        .setReturningAttributes(customAttributes.toArray(new String[0]));

    // 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);
    }
  }

}

相关信息

hadoop 源码目录

相关文章

hadoop AccessControlException 源码

hadoop AnnotatedSecurityInfo 源码

hadoop AuthenticationFilterInitializer 源码

hadoop CompositeGroupsMapping 源码

hadoop Credentials 源码

hadoop FastSaslClientFactory 源码

hadoop FastSaslServerFactory 源码

hadoop GroupMappingServiceProvider 源码

hadoop Groups 源码

hadoop HadoopKerberosName 源码

0  赞