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