/**
  * Obtains the list of the LDAP servers in the order we should talk to, given how this {@link
  * ActiveDirectoryUnixAuthenticationProvider} is configured.
  */
 private List<SocketInfo> obtainLDAPServers(String domainName)
     throws AuthenticationServiceException {
   try {
     return descriptor.obtainLDAPServer(domainName, site, server);
   } catch (NamingException e) {
     LOGGER.log(Level.WARNING, "Failed to find the LDAP service", e);
     throw new AuthenticationServiceException(
         "Failed to find the LDAP service for the domain " + domainName, e);
   }
 }
        @Override
        protected ActiveDirectoryGroupDetails compute(String groupname) {
          boolean problem = false;
          for (String domainName : domainNames) {
            // when we use custom socket factory below, every LDAP operations result
            // in a classloading via context classloader, so we need it to resolve.
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
            try {
              DirContext context =
                  descriptor.bind(bindName, bindPassword, obtainLDAPServers(domainName));

              try {
                final String domainDN = toDC(domainName);

                Attributes group =
                    new LDAPSearchBuilder(context, domainDN)
                        .subTreeScope()
                        .searchOne("(& (cn={0})(objectCategory=group))", groupname);
                if (group == null) {
                  // failed to find it. Fall back to sAMAccountName.
                  // see http://www.nabble.com/Re%3A-Hudson-AD-plug-in-td21428668.html
                  LOGGER.fine("Failed to find " + groupname + " in cn. Trying sAMAccountName");
                  group =
                      new LDAPSearchBuilder(context, domainDN)
                          .subTreeScope()
                          .searchOne("(& (sAMAccountName={0})(objectCategory=group))", groupname);
                  if (group == null) {
                    // Group not found in this domain, try next
                    continue;
                  }
                }
                LOGGER.fine("Found group " + groupname + " : " + group);
                return new ActiveDirectoryGroupDetails(groupname);
              } catch (NamingException e) {
                LOGGER.log(
                    Level.WARNING, "Failed to retrieve user information for " + groupname, e);
                throw new BadCredentialsException(
                    "Failed to retrieve user information for " + groupname, e);
              } finally {
                closeQuietly(context);
              }
            } catch (UsernameNotFoundException e) {
              // everything worked OK but we just didn't find it. This could be just a typo in group
              // name.
              LOGGER.log(
                  Level.FINE,
                  "Failed to find the group " + groupname + " in " + domainName + " domain",
                  e);
            } catch (AuthenticationException e) {
              // something went wrong talking to the server. This should be reported
              LOGGER.log(
                  Level.WARNING,
                  "Failed to find the group " + groupname + " in " + domainName + " domain",
                  e);
              problem = true;
            } finally {
              Thread.currentThread().setContextClassLoader(ccl);
            }
          }

          if (!problem) {
            return null; // group not found anywhere. cache this result
          } else {
            LOGGER.log(
                Level.WARNING,
                "Exhausted all configured domains and could not authenticate against any.");
            throw new UserMayOrMayNotExistException(groupname);
          }
        }
  /**
   * Authenticates and retrieves the user by using the given list of available AD LDAP servers.
   *
   * @param password If this is {@link #NO_AUTHENTICATION}, the authentication is not performed, and
   *     just the retrieval would happen.
   * @throws UsernameNotFoundException The user didn't exist.
   * @return never null
   */
  public UserDetails retrieveUser(
      String username, String password, String domainName, List<SocketInfo> ldapServers) {
    DirContext context;
    String id;
    boolean anonymousBind; // did we bind anonymously?

    // LDAP treats empty password as anonymous bind, so we need to reject it
    if (StringUtils.isEmpty(password)) throw new BadCredentialsException("Empty password");

    if (bindName != null) {
      // two step approach. Use a special credential to obtain DN for the
      // user trying to login, then authenticate.
      try {
        id = username;
        context = descriptor.bind(bindName, bindPassword, ldapServers);
        anonymousBind = false;
      } catch (BadCredentialsException e) {
        throw new AuthenticationServiceException(
            "Failed to bind to LDAP server with the bind name/password", e);
      }
    } else {
      String principalName = getPrincipalName(username, domainName);
      id = principalName.substring(0, principalName.indexOf('@'));
      anonymousBind = password == NO_AUTHENTICATION;
      try {
        // if we are just retrieving the user, try using anonymous bind by empty password (see RFC
        // 2829 5.1)
        // but if that fails, that's not BadCredentialException but UserMayOrMayNotExistException
        context = descriptor.bind(principalName, anonymousBind ? "" : password, ldapServers);
      } catch (BadCredentialsException e) {
        if (anonymousBind)
          // in my observation, if we attempt an anonymous bind and AD doesn't allow it, it still
          // passes the bind method
          // and only fail later when we actually do a query. So perhaps this is a dead path, but
          // I'm leaving it here
          // anyway as a precaution.
          throw new UserMayOrMayNotExistException(
              "Unable to retrieve the user information without bind DN/password configured");
        throw e;
      }
    }

    try {
      // locate this user's record
      final String domainDN = toDC(domainName);

      Attributes user =
          new LDAPSearchBuilder(context, domainDN)
              .subTreeScope()
              .searchOne("(& (userPrincipalName={0})(objectCategory=user))", id);
      if (user == null) {
        // failed to find it. Fall back to sAMAccountName.
        // see http://www.nabble.com/Re%3A-Hudson-AD-plug-in-td21428668.html
        LOGGER.fine("Failed to find " + id + " in userPrincipalName. Trying sAMAccountName");
        user =
            new LDAPSearchBuilder(context, domainDN)
                .subTreeScope()
                .searchOne("(& (sAMAccountName={0})(objectCategory=user))", id);
        if (user == null) {
          throw new UsernameNotFoundException(
              "Authentication was successful but cannot locate the user information for "
                  + username);
        }
      }
      LOGGER.fine("Found user " + id + " : " + user);

      Object dn = user.get("distinguishedName").get();
      if (dn == null)
        throw new AuthenticationServiceException("No distinguished name for " + username);

      if (bindName != null && password != NO_AUTHENTICATION) {
        // if we've used the credential specifically for the bind, we
        // need to verify the provided password to do authentication
        LOGGER.fine("Attempting to validate password for DN=" + dn);
        DirContext test = descriptor.bind(dn.toString(), password, ldapServers);
        // Binding alone is not enough to test the credential. Need to actually perform some query
        // operation.
        // but if the authentication fails this throws an exception
        try {
          new LDAPSearchBuilder(test, domainDN)
              .searchOne("(& (userPrincipalName={0})(objectCategory=user))", id);
        } finally {
          closeQuietly(test);
        }
      }

      Set<GrantedAuthority> groups = resolveGroups(domainDN, dn.toString(), context);
      groups.add(SecurityRealm.AUTHENTICATED_AUTHORITY);

      return new ActiveDirectoryUserDetail(
              id,
              password,
              true,
              true,
              true,
              true,
              groups.toArray(new GrantedAuthority[groups.size()]),
              getStringAttribute(user, "displayName"),
              getStringAttribute(user, "mail"),
              getStringAttribute(user, "telephoneNumber"))
          .updateUserInfo();
    } catch (NamingException e) {
      if (anonymousBind
          && e.getMessage().contains("successful bind must be completed")
          && e.getMessage().contains("000004DC")) {
        // sometimes (or always?) anonymous bind itself will succeed but the actual query will fail.
        // see JENKINS-12619. On my AD the error code is DSID-0C0906DC
        throw new UserMayOrMayNotExistException(
            "Unable to retrieve the user information without bind DN/password configured");
      }

      LOGGER.log(Level.WARNING, "Failed to retrieve user information for " + username, e);
      throw new BadCredentialsException("Failed to retrieve user information for " + username, e);
    } finally {
      closeQuietly(context);
    }
  }