package hudson.plugins.active_directory;
import hudson.security.GroupDetails;
import hudson.security.SecurityRealm;
import hudson.security.UserMayOrMayNotExistException;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationServiceException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.springframework.dao.DataAccessException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import static javax.naming.directory.SearchControls.SUBTREE_SCOPE;
/**
* {@link AuthenticationProvider} with Active Directory, through LDAP.
*
* @author Kohsuke Kawaguchi
* @author James Nord
*/
public class ActiveDirectoryUnixAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider
implements UserDetailsService, GroupDetailsService {
private final String[] domainNames;
private final String site;
private final String bindName, bindPassword;
private final ActiveDirectorySecurityRealm.DesciprotrImpl descriptor;
public ActiveDirectoryUnixAuthenticationProvider(ActiveDirectorySecurityRealm realm) {
this.domainNames = realm.domain.split(",");
this.site = realm.site;
this.bindName = realm.bindName;
this.bindPassword = realm.bindPassword==null ? null : realm.bindPassword.toString();
this.descriptor = realm.getDescriptor();
}
/**
* We'd like to implement {@link UserDetailsService} ideally, but in short of keeping the manager user/password,
* we can't do so. In Active Directory authentication, we should support SPNEGO/Kerberos and
* that should eliminate the need for the "remember me" service.
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
throw new UsernameNotFoundException("Active-directory plugin doesn't support user retrieval");
}
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// active directory authentication is not by comparing clear text password,
// so there's nothing to do here.
}
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails userDetails = null;
for (String domainName : domainNames) {
try {
userDetails = retrieveUser(username, authentication, domainName);
}
catch (BadCredentialsException bce) {
LOGGER.log(Level.WARNING,"Credential exception tying to authenticate against " + domainName + " domain",bce);
}
if (userDetails != null) {
break;
}
}
if (userDetails == null) {
LOGGER.log(Level.WARNING,"Exhausted all configured domains and could not authenticat against any.");
throw new BadCredentialsException("Either no such user '"+username+"' or incorrect password");
}
return userDetails;
}
private UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication, String domainName) throws AuthenticationException {
// 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 {
String password = null;
if(authentication!=null)
password = (String) authentication.getCredentials();
List<SocketInfo> ldapServers;
try {
ldapServers = descriptor.obtainLDAPServer(domainName,site);
} 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);
}
return retrieveUser(username, password, domainName, ldapServers);
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
}
/**
* Retrieves the user by using the given list of available AD LDAP servers.
*
* @param domainName
*/
public UserDetails retrieveUser(String username, String password, String domainName, List<SocketInfo> ldapServers) {
DirContext context;
String id;
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);
} 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('@'));
context = descriptor.bind(principalName, password, ldapServers);
}
try {
// locate this user's record
SearchControls controls = new SearchControls();
controls.setSearchScope(SUBTREE_SCOPE);
NamingEnumeration<SearchResult> renum = context.search(toDC(domainName),"(& (userPrincipalName={0})(objectClass=user))",
new Object[]{id}, controls);
if(!renum.hasMore()) {
// 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");
renum = context.search(toDC(domainName),"(& (sAMAccountName={0})(objectClass=user))",
new Object[]{id},controls);
if(!renum.hasMore()) {
throw new BadCredentialsException("Authentication was successful but cannot locate the user information for "+username);
}
}
SearchResult result = renum.next();
if (bindName!=null) {
// if we've used the credential specifically for the bind, we need to verify the provided password.
Object dn = result.getAttributes().get("distinguishedName").get();
if (dn==null)
throw new BadCredentialsException("No distinguished name for "+username);
LOGGER.fine("Attempting to validate password for DN="+dn);
DirContext test = descriptor.bind(dn.toString(), password, ldapServers);
test.close();
}
Set<GrantedAuthority> groups = resolveGroups(result.getAttributes(), context);
groups.add(SecurityRealm.AUTHENTICATED_AUTHORITY);
context.close();
return new ActiveDirectoryUserDetail(
id, password,
true, true, true, true,
groups.toArray(new GrantedAuthority[groups.size()])
);
} catch (NamingException e) {
LOGGER.log(Level.WARNING,"Failed to retrieve user information for "+username,e);
throw new BadCredentialsException("Failed to retrieve user information for "+username,e);
}
}
/**
* Returns the full user principal name of the form "joe@europe.contoso.com".
*
* If people type in 'foo@bar' or 'bar\\foo', it should be treated as 'foo@bar.acme.org'
*/
private String getPrincipalName(String username, String domainName) {
String principalName;
int slash = username.indexOf('\\');
if (slash>0) {
principalName = username.substring(slash+1)+'@'+username.substring(0,slash)+'.'+domainName;
} else
if (username.contains("@"))
principalName = username + '.' + domainName;
else
principalName = username + '@' + domainName;
return principalName;
}
/**
* Recursively resolve group memberships of the given identity and returns them all as a set.
*
* @param context
* Used for making queries.
*/
private Set<GrantedAuthority> resolveGroups(Attributes identity, DirContext context) throws NamingException {
Set<GrantedAuthority> groups = new HashSet<GrantedAuthority>();
LinkedList<Attributes> membershipList = new LinkedList<Attributes>();
membershipList.add(identity);
while (!membershipList.isEmpty()) {
identity = membershipList.removeFirst();
Attribute memberOf = identity.get("memberOf");
if (memberOf == null) continue;
for (int i=0; i < memberOf.size() ; i++) {
if (LOGGER.isLoggable(Level.FINE))
LOGGER.fine(identity.get("CN").get()+" is a member of "+memberOf.get(i));
Attributes group = context.getAttributes("\"" + memberOf.get(i) + '"',
new String[] {"CN", "memberOf"});
Attribute cn = group.get("CN");
if (groups.add(new GrantedAuthorityImpl(cn.get().toString()))) {
membershipList.add(group); // recursively look for groups that this group is a member of.
}
}
}
return groups;
}
private static String toDC(String domainName) {
StringBuilder buf = new StringBuilder();
for (String token : domainName.split("\\.")) {
if(token.length()==0) continue; // defensive check
if(buf.length()>0) buf.append(",");
buf.append("DC=").append(token);
}
return buf.toString();
}
private static final Logger LOGGER = Logger.getLogger(ActiveDirectoryUnixAuthenticationProvider.class.getName());
public GroupDetails loadGroupByGroupname(String groupname) {
throw new UserMayOrMayNotExistException(groupname);
}
}