Skip to content

Commit

Permalink
[FIXED JENKINS-39065] Fail-over user to fallback when there are authe…
Browse files Browse the repository at this point in the history
…ntication issues (#52)

[JENKINS-39065] Fail-over user to fallback when there are authentication issues
  • Loading branch information
fbelzunc committed Jun 20, 2017
1 parent 4e46466 commit 61feb0c
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 16 deletions.
@@ -0,0 +1,51 @@
package hudson.plugins.active_directory;

/*
* The MIT License
*
* Copyright (c) 2017-2018, Félix Belzunce Arcos, CloudBees, Inc., and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;

/**
* Class to fall back into the Jenkins Internal Database in case of any {@link javax.naming.NamingException}
*/
public class ActiveDirectoryInternalUsersDatabase {

/**
* The Jenkins internal user to fall back in order to access Jenkins
* in case of a {@link javax.naming.NamingException}
*/
private final String jenkinsInternalUser;

@DataBoundConstructor
public ActiveDirectoryInternalUsersDatabase(String jenkinsInternalUser) {
this.jenkinsInternalUser = jenkinsInternalUser;
}

@Restricted(NoExternalUse.class)
public String getJenkinsInternalUser() {
return jenkinsInternalUser;
}
}
Expand Up @@ -212,12 +212,16 @@ public class ActiveDirectorySecurityRealm extends AbstractPasswordBasedSecurityR
*/
protected TlsConfiguration tlsConfiguration;

/**
* The Jenkins internal user to fall back in case f {@link NamingException}
*/
protected ActiveDirectoryInternalUsersDatabase internalUsersDatabase;

/**
* The threadPool to update the cache on background
*/
protected transient ExecutorService threadPoolExecutor;


public ActiveDirectorySecurityRealm(String domain, String site, String bindName, String bindPassword, String server) {
this(domain, site, bindName, bindPassword, server, GroupLookupStrategy.AUTO, false);
}
Expand All @@ -227,7 +231,7 @@ public ActiveDirectorySecurityRealm(String domain, String site, String bindName,
}

public ActiveDirectorySecurityRealm(String domain, String site, String bindName,
String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups) {
String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups) {
this(domain, site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, null);
}

Expand All @@ -241,11 +245,15 @@ public ActiveDirectorySecurityRealm(String domain, List<ActiveDirectoryDomain> d
this(domain, domains, site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, customDomain, cache, startTls, TlsConfiguration.TRUST_ALL_CERTIFICATES);
}

public ActiveDirectorySecurityRealm(String domain, List<ActiveDirectoryDomain> domains, String site, String bindName,
String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups, Boolean customDomain, CacheConfiguration cache, Boolean startTls, TlsConfiguration tlsConfiguration) {
this(domain, domains, site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, customDomain, cache, startTls, TlsConfiguration.TRUST_ALL_CERTIFICATES, null);
}

@DataBoundConstructor
// as Java signature, this binding doesn't make sense, so please don't use this constructor
public ActiveDirectorySecurityRealm(String domain, List<ActiveDirectoryDomain> domains, String site, String bindName,
String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups, Boolean customDomain, CacheConfiguration cache, Boolean startTls, TlsConfiguration tlsConfiguration) {
String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups, Boolean customDomain, CacheConfiguration cache, Boolean startTls, TlsConfiguration tlsConfiguration, ActiveDirectoryInternalUsersDatabase internalUsersDatabase) {
if (customDomain!=null && !customDomain)
domains = null;
this.domain = fixEmpty(domain);
Expand All @@ -259,6 +267,7 @@ public ActiveDirectorySecurityRealm(String domain, List<ActiveDirectoryDomain> d
this.cache = cache;
this.tlsConfiguration = tlsConfiguration;
this.startTls = startTls;
this.internalUsersDatabase = internalUsersDatabase;
}

@DataBoundSetter
Expand All @@ -273,6 +282,17 @@ public CacheConfiguration getCache() {
}
return cache;
}

@Restricted(NoExternalUse.class)
public String getJenkinsInternalUser() {
return internalUsersDatabase == null ? null : internalUsersDatabase.getJenkinsInternalUser();
}

@Restricted(NoExternalUse.class)
public ActiveDirectoryInternalUsersDatabase getInternalUsersDatabase() {
return internalUsersDatabase != null && internalUsersDatabase.getJenkinsInternalUser() != null && internalUsersDatabase.getJenkinsInternalUser().isEmpty() ? null : internalUsersDatabase;
}

@Restricted(NoExternalUse.class)
public Boolean isStartTls() {
return startTls;
Expand Down Expand Up @@ -516,7 +536,7 @@ private boolean isTrustAllCertificatesEnabled(TlsConfiguration tlsConfiguration)
private static boolean WARNED = false;

@Deprecated
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props) {
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props) throws NamingException {
return bind(principalName, password, ldapServers, props, null);
}

Expand All @@ -526,7 +546,7 @@ public DirContext bind(String principalName, String password, List<SocketInfo> l
* In a real deployment, often there are servers that don't respond or
* otherwise broken, so try all the servers.
*/
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props, TlsConfiguration tlsConfiguration) {
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props, TlsConfiguration tlsConfiguration) throws NamingException {
// in a AD forest, it'd be mighty nice to be able to login as "joe"
// as opposed to "joe@europe",
// but the bind operation doesn't appear to allow me to do so.
Expand Down Expand Up @@ -574,7 +594,8 @@ public DirContext bind(String principalName, String password, List<SocketInfo> l
}
}
// if all the attempts failed
throw new BadCredentialsException("Either no such user '" + principalName + "' or incorrect password", namingException);
LOGGER.log(Level.WARNING, "All attempts to login failed for user {0}", principalName);
throw namingException;
}

/**
Expand All @@ -584,7 +605,7 @@ public DirContext bind(String principalName, String password, List<SocketInfo> l
* otherwise broken, so try all the servers.
*/
@Deprecated
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers) {
public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers) throws NamingException {
return bind(principalName, password, ldapServers, new Hashtable<String, String>());
}

Expand Down
Expand Up @@ -27,7 +27,9 @@
import com.google.common.util.concurrent.UncheckedExecutionException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Util;
import hudson.model.User;
import hudson.security.GroupDetails;
import hudson.security.HudsonPrivateSecurityRealm;
import hudson.security.SecurityRealm;
import hudson.security.UserMayOrMayNotExistException;
import hudson.util.DaemonThreadFactory;
Expand Down Expand Up @@ -91,6 +93,11 @@ public class ActiveDirectoryUnixAuthenticationProvider extends AbstractActiveDir

private GroupLookupStrategy groupLookupStrategy;

/**
* The internal {@link User} to fall back when {@link NamingException} happens
*/
private final ActiveDirectoryInternalUsersDatabase activeDirectoryInternalUser;

protected static final String DN_FORMATTED = "distinguishedNameFormatted";

/**
Expand Down Expand Up @@ -175,6 +182,7 @@ public ActiveDirectoryUnixAuthenticationProvider(ActiveDirectorySecurityRealm re
this.site = realm.site;
this.domains = realm.domains;
this.groupLookupStrategy = realm.getGroupLookupStrategy();
this.activeDirectoryInternalUser = realm.internalUsersDatabase;
this.descriptor = realm.getDescriptor();
this.cache = realm.cache;

Expand Down Expand Up @@ -208,14 +216,34 @@ public ActiveDirectoryUnixAuthenticationProvider(ActiveDirectorySecurityRealm re
protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
try {
// this is more seriously error, indicating a failure to search
List<BadCredentialsException> errors = new ArrayList<BadCredentialsException>();
List<AuthenticationException> errors = new ArrayList<AuthenticationException>();

// this is lesser error, in that we searched and the user was not found
List<UsernameNotFoundException> notFound = new ArrayList<UsernameNotFoundException>();

for (ActiveDirectoryDomain domain : domains) {
try {
return retrieveUser(username, authentication, domain);
} catch (NamingException ne) {
if (activeDirectoryInternalUser != null && activeDirectoryInternalUser.getJenkinsInternalUser() != null && username.equals(activeDirectoryInternalUser.getJenkinsInternalUser())) {
LOGGER.log(Level.WARNING, String.format("Looking into Jenkins Internal Users Database for user %s", username));
User internalUser = hudson.model.User.get(username);
HudsonPrivateSecurityRealm.Details hudsonPrivateSecurityRealm = internalUser.getProperty(HudsonPrivateSecurityRealm.Details.class);
String password = "";
if (authentication.getCredentials() instanceof String) {
password = (String) authentication.getCredentials();
}
if (hudsonPrivateSecurityRealm.isPasswordCorrect(password)) {
LOGGER.log(Level.INFO, String.format("Falling back into the internal user %s", username));
return new ActiveDirectoryUserDetail(username, password, true, true, true, true, hudsonPrivateSecurityRealm.getAuthorities(), internalUser.getDisplayName(), "", "");
} else {
LOGGER.log(Level.WARNING, String.format("Credential exception trying to authenticate against %s domain", domain.getName()), ne);
errors.add(new MultiCauseUserMayOrMayNotExistException("We can't tell if the user exists or not: " + username, notFound));
}
} else {
LOGGER.log(Level.WARNING, String.format("Communications issues when trying to authenticate against %s domain", domain.getName()), ne);
errors.add(new MultiCauseUserMayOrMayNotExistException("We can't tell if the user exists or not: " + username, notFound));
}
} catch (UsernameNotFoundException e) {
notFound.add(e);
} catch (BadCredentialsException bce) {
Expand Down Expand Up @@ -263,7 +291,7 @@ protected boolean canRetrieveUserByName(ActiveDirectoryDomain domain) {
* @param authentication
* null if we are just retrieving the said user, instead of trying to authenticate.
*/
private UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication, ActiveDirectoryDomain domain) throws AuthenticationException {
private UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication, ActiveDirectoryDomain domain) throws AuthenticationException, NamingException {
// 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();
Expand All @@ -283,12 +311,12 @@ private UserDetails retrieveUser(String username, UsernamePasswordAuthentication
* 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(ActiveDirectoryDomain domain) throws AuthenticationServiceException {
private List<SocketInfo> obtainLDAPServers(ActiveDirectoryDomain domain) throws AuthenticationServiceException, NamingException {
try {
return descriptor.obtainLDAPServer(domain);
} 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 "+ domain.getName(), e);
LOGGER.log(Level.WARNING, "Failed to find the LDAP service for the domain {0}", domain.getName());
throw e;
}
}

Expand All @@ -303,15 +331,15 @@ private List<SocketInfo> obtainLDAPServers(ActiveDirectoryDomain domain) throws
* @return never null
*/
@SuppressFBWarnings(value = "ES_COMPARING_PARAMETER_STRING_WITH_EQ", justification = "Intentional instance check.")
public UserDetails retrieveUser(final String username, final String password, final ActiveDirectoryDomain domain, final List<SocketInfo> ldapServers) {
public UserDetails retrieveUser(final String username, final String password, final ActiveDirectoryDomain domain, final List<SocketInfo> ldapServers) throws NamingException {
UserDetails userDetails;
String hashKey = username + "@@" + DigestUtils.sha1Hex(password);
final String bindName = domain.getBindName();
final String bindPassword = Secret.toString(domain.getBindPassword());
try {
final ActiveDirectoryUserDetail[] cacheMiss = new ActiveDirectoryUserDetail[1];
userDetails = userCache.get(hashKey, new Callable<UserDetails>() {
public UserDetails call() throws AuthenticationException {
public UserDetails call() throws AuthenticationException, NamingException {
DirContext context;
boolean anonymousBind = false; // did we bind anonymously?

Expand All @@ -329,7 +357,7 @@ public UserDetails call() throws AuthenticationException {
try {
context = descriptor.bind(bindName, bindPassword, ldapServers, props, tlsConfiguration);
anonymousBind = false;
} catch (BadCredentialsException e) {
} catch (NamingException e) {
throw new AuthenticationServiceException("Failed to bind to LDAP server with the bind name/password", e);
}
} else {
Expand Down Expand Up @@ -400,6 +428,9 @@ public UserDetails call() throws AuthenticationException {
);
return cacheMiss[0];
} catch (NamingException e) {
if (activeDirectoryInternalUser != null) {
throw 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
Expand Down Expand Up @@ -431,6 +462,25 @@ public void run() {
}
}
});
if (activeDirectoryInternalUser != null && activeDirectoryInternalUser.getJenkinsInternalUser() != null&& username.equals(activeDirectoryInternalUser.getJenkinsInternalUser())) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
final String threadName = Thread.currentThread().getName();
Thread.currentThread().setName(threadName + " updating-internal-jenkins-database-for-user" + cacheMiss[0].getUsername());
LOGGER.log(Level.FINEST, "Starting the Jenkins Internal Database update {0}", new Date());
try {
long t0 = System.currentTimeMillis();
cacheMiss[0].updatePasswordInJenkinsInternalDatabase(username, password);
LOGGER.log(Level.FINEST, "Finished the password update {0}", new Date());
long t1 = System.currentTimeMillis();
LOGGER.log(Level.FINE, "The password update for user {0} took {1} msec", new Object[]{cacheMiss[0].getUsername(), String.valueOf(t1-t0)});
} finally {
Thread.currentThread().setName(threadName);
}
}
});
}

}
} catch (UncheckedExecutionException e) {
Expand All @@ -456,7 +506,7 @@ public void run() {
public GroupDetails loadGroupByGroupname(final String groupname) {
try {
return groupCache.get(groupname, new Callable<ActiveDirectoryGroupDetails>() {
public ActiveDirectoryGroupDetails call() {
public ActiveDirectoryGroupDetails call() throws NamingException {
for (ActiveDirectoryDomain domain : domains) {
if (domain==null) {
throw new UserMayOrMayNotExistException("Unable to retrieve group information without bind DN/password configured");
Expand Down

0 comments on commit 61feb0c

Please sign in to comment.