/* * The MIT License * * Copyright (c) 2008-2014, Kohsuke Kawaguchi, 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. */ package hudson.plugins.active_directory; import com.google.common.collect.Lists; import com.sun.jndi.ldap.LdapCtxFactory; import com4j.typelibs.ado20.ClassFactory; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import groovy.lang.Binding; import hudson.Extension; import hudson.Functions; import hudson.init.Terminator; import hudson.model.AbstractDescribableImpl; import hudson.model.AdministrativeMonitor; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.security.AbstractPasswordBasedSecurityRealm; import hudson.security.AuthorizationStrategy; import hudson.security.GroupDetails; import hudson.security.SecurityRealm; import hudson.security.TokenBasedRememberMeServices2; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import hudson.util.Secret; import hudson.util.spring.BeanBuilder; import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.AuthenticationManager; import org.acegisecurity.BadCredentialsException; import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; import org.acegisecurity.userdetails.UserDetails; import org.acegisecurity.userdetails.UserDetailsService; import org.acegisecurity.userdetails.UsernameNotFoundException; import org.apache.commons.io.IOUtils; import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.springframework.dao.DataAccessException; import org.springframework.web.context.WebApplicationContext; import javax.naming.Context; 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.InitialDirContext; import javax.naming.ldap.LdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import javax.net.ssl.SSLSocketFactory; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.ObjectStreamException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.logging.Level; import java.util.logging.Logger; import static hudson.Util.*; import static hudson.plugins.active_directory.ActiveDirectoryUnixAuthenticationProvider.*; /** * {@link SecurityRealm} that talks to Active Directory. * * @author Kohsuke Kawaguchi */ public class ActiveDirectorySecurityRealm extends AbstractPasswordBasedSecurityRealm { /** * Represent the old Active Directory Domain * * <p> * We need to keep this as transient in order to be able to use readResolve * to migrate the old descriptor to the newone. * * <p> * This has been deprecated since {@link ActiveDirectoryDomain} */ public transient String domain; /** * Represent the old Active Directory Domain Controllers * * <p> * We need to keep this as transient in order to be able to use readResolve * to migrate the old descriptor to the newone. * * <p> * This has been deprecated since {@link ActiveDirectoryDomain} */ public transient String server; /** * List of {@link ActiveDirectoryDomain} * */ public List<ActiveDirectoryDomain> domains; /** * Active directory site (which specifies the physical concentration of the * servers), if any. If the value is non-null, we'll only contact servers in * this site. * * <p> * On Windows, I'm assuming ADSI takes care of everything automatically. * * <p> * We need to keep this as transient in order to be able to use readResolve * to migrate the old descriptor to the newone. */ public transient final String site; /** * Represent the old bindName * * <p> * We need to keep this as transient in order to be able to use readResolve * to migrate the old descriptor to the new one. * * <p> * This has been deprecated @since Jenkins 2.1 */ public transient String bindName; /** * Represent the old bindPassword * * <p> * We need to keep this as transient in order to be able to use readResolve * to migrate the old descriptor to the new one. * * <p> * This has been deprecated @since Jenkins 2.1 */ public transient Secret bindPassword; /** * If true enable startTls in case plain communication is used. In case the plugin * is configured to use TLS then this option will not have any impact. */ public Boolean startTls; private GroupLookupStrategy groupLookupStrategy; /** * If true, Jenkins ignores Active Directory groups that are not being used by the active Authorization Strategy. * This can significantly improve performance in environments with a large number of groups * but a small number of corresponding rules defined by the Authorization Strategy. * Groups are considered as used if they are returned by {@link AuthorizationStrategy#getGroups()}. */ public final boolean removeIrrelevantGroups; /** * Cache of the Active Directory plugin */ protected CacheConfiguration cache; /** * Ldap extra properties */ protected List<EnvironmentProperty> environmentProperties; /** * Selects the SSL strategy to follow on the TLS connections * * <p> * Even if we are not using any of the TLS ports (3269/636) the plugin will try to establish a TLS channel * using startTLS. Because of this, we need to be able to specify the SSL strategy on the plugin * * <p> * For the moment there are two possible values: trustAllCertificates and trustStore. */ protected TlsConfiguration tlsConfiguration; /** * 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); } public ActiveDirectorySecurityRealm(String domain, String site, String bindName, String bindPassword, String server, GroupLookupStrategy groupLookupStrategy) { this(domain,site,bindName,bindPassword,server,groupLookupStrategy,false); } public ActiveDirectorySecurityRealm(String domain, String site, String bindName, String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups) { this(domain, site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, null); } public ActiveDirectorySecurityRealm(String domain, String site, String bindName, String bindPassword, String server, GroupLookupStrategy groupLookupStrategy, boolean removeIrrelevantGroups, CacheConfiguration cache) { this(domain, Lists.newArrayList(new ActiveDirectoryDomain(domain, server)), site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, domain!=null, cache, true); } 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) { this(domain, domains, site, bindName, bindPassword, server, groupLookupStrategy, removeIrrelevantGroups, customDomain, cache, startTls, TlsConfiguration.TRUST_ALL_CERTIFICATES); } @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) { if (customDomain!=null && !customDomain) domains = null; this.domain = fixEmpty(domain); this.server = fixEmpty(server); this.domains = domains; this.site = fixEmpty(site); this.bindName = fixEmpty(bindName); this.bindPassword = Secret.fromString(fixEmpty(bindPassword)); this.groupLookupStrategy = groupLookupStrategy; this.removeIrrelevantGroups = removeIrrelevantGroups; this.cache = cache; this.tlsConfiguration = tlsConfiguration; this.startTls = startTls; } @DataBoundSetter public void setEnvironmentProperties(List<EnvironmentProperty> environmentProperties) { this.environmentProperties = environmentProperties; } @Restricted(NoExternalUse.class) public CacheConfiguration getCache() { if (cache != null && (cache.getSize() == 0 || cache.getTtl() == 0)) { return null; } return cache; } @Restricted(NoExternalUse.class) public Boolean isStartTls() { return startTls; } public Integer getSize() { return cache == null ? null : cache.getSize(); } public Integer getTtl() { return cache == null ? null : cache.getTtl(); } // for jelly use only @Restricted(NoExternalUse.class) public List<EnvironmentProperty> getEnvironmentProperties() { return environmentProperties; } public GroupLookupStrategy getGroupLookupStrategy() { if (groupLookupStrategy==null) return GroupLookupStrategy.AUTO; return groupLookupStrategy; } // for jelly use only @Restricted(NoExternalUse.class) public TlsConfiguration getTlsConfiguration() { return tlsConfiguration; } public SecurityComponents createSecurityComponents() { BeanBuilder builder = new BeanBuilder(getClass().getClassLoader()); Binding binding = new Binding(); binding.setVariable("realm", this); InputStream i = getClass().getResourceAsStream("ActiveDirectory.groovy"); try { builder.parse(i, binding); } finally { IOUtils.closeQuietly(i); } WebApplicationContext context = builder.createApplicationContext(); //final AbstractActiveDirectoryAuthenticationProvider adp = findBean(AbstractActiveDirectoryAuthenticationProvider.class, context); findBean(AbstractActiveDirectoryAuthenticationProvider.class, context); //Keeping the call because there might be side effects? final UserDetailsService uds = findBean(UserDetailsService.class, context); TokenBasedRememberMeServices2 rms = new TokenBasedRememberMeServices2() { public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { try { return super.autoLogin(request, response); } catch (Exception e) {// TODO: this check is made redundant with 1.556, but needed with earlier versions cancelCookie(request, response, "Failed to handle remember-me cookie: "+Functions.printThrowable(e)); return null; } } }; rms.setUserDetailsService(uds); rms.setKey(Hudson.getInstance().getSecretKey()); rms.setParameter("remember_me"); // this is the form field name in login.jelly return new SecurityComponents( findBean(AuthenticationManager.class, context), uds, rms); } @Restricted(NoExternalUse.class) public List<ActiveDirectoryDomain> getDomains() { return domains; } public Object readResolve() throws ObjectStreamException { if (domain != null) { this.domains = new ArrayList<ActiveDirectoryDomain>(); domain = domain.trim(); String[] oldDomains = domain.split(","); for (String oldDomain : oldDomains) { oldDomain = oldDomain.trim(); this.domains.add(new ActiveDirectoryDomain(oldDomain, server)); } } List <ActiveDirectoryDomain> activeDirectoryDomains = this.getDomains(); // JENKINS-14281 On Windows domain can be indeed null if (activeDirectoryDomains!= null) { // JENKINS-39375 Support a different bindUser per domain if (bindName != null && bindPassword != null) { for (ActiveDirectoryDomain activeDirectoryDomain : activeDirectoryDomains) { activeDirectoryDomain.bindName = bindName; activeDirectoryDomain.bindPassword = bindPassword; } } // JENKINS-39423 Make site independent of each domain if (site != null) { for (ActiveDirectoryDomain activeDirectoryDomain : activeDirectoryDomains) { activeDirectoryDomain.site = site; } } } if (startTls == null) { this.startTls = true; } return this; } @Override public DescriptorImpl getDescriptor() { return (DescriptorImpl) super.getDescriptor(); } /** * Authentication test. */ public void doAuthTest(StaplerRequest req, StaplerResponse rsp, @QueryParameter String username, @QueryParameter String password) throws IOException, ServletException { // require the administrator permission since this is full of debug info. Hudson.getInstance().checkPermission(Hudson.ADMINISTER); StringWriter out = new StringWriter(); PrintWriter pw = new PrintWriter(out); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); try { UserDetailsService uds = getAuthenticationProvider(); if (uds instanceof ActiveDirectoryUnixAuthenticationProvider) { ActiveDirectoryUnixAuthenticationProvider p = (ActiveDirectoryUnixAuthenticationProvider) uds; DescriptorImpl descriptor = getDescriptor(); for (ActiveDirectoryDomain domain : domains) { try { pw.println("Domain= " + domain.getName() + " site= "+ domain.getSite()); List<SocketInfo> ldapServers = descriptor.obtainLDAPServer(domain); pw.println("List of domain controllers: "+ldapServers); for (SocketInfo ldapServer : ldapServers) { pw.println("Trying a domain controller at "+ldapServer); try { UserDetails d = p.retrieveUser(username, password, domain, Collections.singletonList(ldapServer)); pw.println("Authenticated as "+d); } catch (AuthenticationException e) { e.printStackTrace(pw); } } } catch (NamingException e) { pw.println("Failing to resolve domain controllers"); e.printStackTrace(pw); } } } else { pw.println("Using Windows ADSI. No diagnostics available."); } } catch (Exception e) { e.printStackTrace(pw); } finally { Thread.currentThread().setContextClassLoader(ccl); } req.setAttribute("output", out.toString()); req.getView(this, "test.jelly").forward(req, rsp); } @Restricted(DoNotUse.class) public void shutDownthreadPoolExecutors() { threadPoolExecutor.shutdown(); } @Extension public static final class DescriptorImpl extends Descriptor<SecurityRealm> { public String getDisplayName() { return Messages.DisplayName(); } @Override public String getHelpFile() { return "/plugin/active-directory/help/realm.html"; } /** * If true, we can do ADSI/COM based look up that's far more reliable. * False if we need to do the authentication in pure Java via * {@link ActiveDirectoryUnixAuthenticationProvider} */ public boolean canDoNativeAuth() { if (!Functions.isWindows()) return false; try { ClassFactory.createConnection().dispose(); return true; } catch (Throwable t) { if (!WARNED) { LOGGER.log(Level.INFO,"COM4J isn't working. Falling back to non-native authentication",t); WARNED = true; } return false; } } public ListBoxModel doFillSizeItems() { ListBoxModel listBoxModel = new ListBoxModel(); listBoxModel.add("10 elements", "10"); listBoxModel.add("20 elements", "20"); listBoxModel.add("50 elements", "50"); listBoxModel.add("100 elements", "100"); listBoxModel.add("200 elements", "200"); listBoxModel.add("256 elements", "256"); listBoxModel.add("500 elements", "500"); listBoxModel.add("1000 elements", "1000"); return listBoxModel; } public ListBoxModel doFillTtlItems() { ListBoxModel listBoxModel = new ListBoxModel(); listBoxModel.add("30 sec", "30"); listBoxModel.add("1 min", "60"); listBoxModel.add("5 min", "300"); listBoxModel.add("10 min", "600"); listBoxModel.add("15 min", "900"); listBoxModel.add("30 min", "1800"); listBoxModel.add("1 hour", "3600"); return listBoxModel; } public ListBoxModel doFillGroupLookupStrategyItems() { ListBoxModel model = new ListBoxModel(); for (GroupLookupStrategy e : GroupLookupStrategy.values()) { model.add(e.getDisplayName(),e.name()); } return model; } public ListBoxModel doFillTlsConfigurationItems() { ListBoxModel model = new ListBoxModel(); for (TlsConfiguration tlsConfiguration : TlsConfiguration.values()) { model.add(tlsConfiguration.getDisplayName(),tlsConfiguration.name()); } return model; } private boolean isTrustAllCertificatesEnabled(TlsConfiguration tlsConfiguration) { return (tlsConfiguration == null || TlsConfiguration.TRUST_ALL_CERTIFICATES.equals(tlsConfiguration)); } private static boolean WARNED = false; @Deprecated public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers, Hashtable<String, String> props) { return bind(principalName, password, ldapServers, props, null); } /** * Binds to the server using the specified username/password. * <p> * 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) { // 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. Hashtable<String, String> newProps = new Hashtable<String, String>(); // Sometimes might be useful to ignore referral. Use this System property is under the user risk Boolean ignoreReferrals = Boolean.valueOf(System.getProperty("hudson.plugins.active_directory.referral.ignore", "false")); if (!ignoreReferrals) { newProps.put(Context.REFERRAL, "follow"); } else { newProps.put(Context.REFERRAL, "ignore"); } newProps.put("java.naming.ldap.attributes.binary","tokenGroups objectSid"); if (FORCE_LDAPS && isTrustAllCertificatesEnabled(tlsConfiguration)) { newProps.put("java.naming.ldap.factory.socket", TrustAllSocketFactory.class.getName()); } newProps.putAll(props); NamingException namingException = null; for (SocketInfo ldapServer : ldapServers) { try { LdapContext context = bind(principalName, password, ldapServer, newProps); LOGGER.fine("Bound to " + ldapServer); return context; } catch (javax.naming.AuthenticationException e) { // if the authentication failed (as opposed to a communication problem with the server), // don't retry, because if this is because of a wrong password, we can end up locking // the user out by causing multiple failed attempts. // error code 49 (LdapClient.LDAP_INVALID_CREDENTIALS) maps to this exception in LdapCtx.mapErrorCode // see http://confluence.atlassian.com/display/CONFKB/LDAP+Error+Code+49 and http://www-01.ibm.com/support/docview.wss?uid=swg21290631 // for subcodes within this error. // it seems like we can be clever about checking subcode to decide if we retry or not, // but I'm erring on the safe side as I'm not sure how reliable the code is, and maybe // servers can be configured to hide the distinction between "no such user" and "bad password" // to reveal what user names are available. LOGGER.log(Level.WARNING, "Failed to authenticate while binding to "+ldapServer, e); throw new BadCredentialsException("Either no such user '" + principalName + "' or incorrect password", namingException); } catch (NamingException e) { LOGGER.log(Level.WARNING, "Failed to bind to "+ldapServer, e); namingException = e; // retry } } // if all the attempts failed throw new BadCredentialsException("Either no such user '" + principalName + "' or incorrect password", namingException); } /** * Binds to the server using the specified username/password. * <p> * In a real deployment, often there are servers that don't respond or * otherwise broken, so try all the servers. */ @Deprecated public DirContext bind(String principalName, String password, List<SocketInfo> ldapServers) { return bind(principalName, password, ldapServers, new Hashtable<String, String>()); } private void customizeLdapProperty(Hashtable<String, String> props, String propName) { String prop = System.getProperty(propName, null); if (prop != null) { props.put(propName, prop); } } /** Lookups for hardcoded LDAP properties if they are specified as System properties and uses them */ private void customizeLdapProperties(Hashtable<String, String> props) { customizeLdapProperty(props, "com.sun.jndi.ldap.connect.timeout"); customizeLdapProperty(props, "com.sun.jndi.ldap.read.timeout"); } @IgnoreJRERequirement @Deprecated private LdapContext bind(String principalName, String password, SocketInfo server, Hashtable<String, String> props) throws NamingException { return bind(principalName, password, server, props, null); } @IgnoreJRERequirement private LdapContext bind(String principalName, String password, SocketInfo server, Hashtable<String, String> props, TlsConfiguration tlsConfiguration) throws NamingException { String ldapUrl = (FORCE_LDAPS?"ldaps://":"ldap://") + server + '/'; String oldName = Thread.currentThread().getName(); Thread.currentThread().setName("Connecting to "+ldapUrl+" : "+oldName); LOGGER.fine("Connecting to " + ldapUrl); try { props.put(Context.PROVIDER_URL, ldapUrl); props.put("java.naming.ldap.version", "3"); customizeLdapProperties(props); LdapContext context = (LdapContext)LdapCtxFactory.getLdapCtxInstance(ldapUrl, props); boolean isStartTls = true; SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm(); if (securityRealm instanceof ActiveDirectorySecurityRealm) { ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm; isStartTls= activeDirectorySecurityRealm.isStartTls(); } if (!FORCE_LDAPS && isStartTls) { // try to upgrade to TLS if we can, but failing to do so isn't fatal // see http://download.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html try { StartTlsResponse rsp = (StartTlsResponse)context.extendedOperation(new StartTlsRequest()); if (isTrustAllCertificatesEnabled(tlsConfiguration)) { rsp.negotiate((SSLSocketFactory)TrustAllSocketFactory.getDefault()); } else { rsp.negotiate(); } LOGGER.fine("Connection upgraded to TLS"); } catch (NamingException e) { LOGGER.log(Level.FINE, "Failed to start TLS. Authentication will be done via plain-text LDAP", e); } catch (IOException e) { LOGGER.log(Level.FINE, "Failed to start TLS. Authentication will be done via plain-text LDAP", e); } } if (principalName==null || password==null || password.equals("")) { // anonymous bind. LDAP uses empty password as a signal to anonymous bind (RFC 2829 5.1), // which means it can never be the actual user password. context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "none"); LOGGER.fine("Binding anonymously to "+ldapUrl); } else { // authenticate after upgrading to TLS, so that the credential won't go in clear text context.addToEnvironment(Context.SECURITY_PRINCIPAL, principalName); context.addToEnvironment(Context.SECURITY_CREDENTIALS, password); LOGGER.fine("Binding as "+principalName+" to "+ldapUrl); } // this is supposed to cause the LDAP bind operation with the server, // but I notice that AD may still accept this and yet fail to search later, // when I tried anonymous bind. // if I do specify a wrong credential, this seems to fail. context.reconnect(null); return context; // worked } finally { Thread.currentThread().setName(oldName); } } /** * Creates {@link DirContext} for accesssing DNS. */ public DirContext createDNSLookupContext() throws NamingException { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); env.put("java.naming.provider.url", "dns:"); return new InitialDirContext(env); } @Deprecated public List<SocketInfo> obtainLDAPServer(String domainName, String site, String preferredServer) throws NamingException { return obtainLDAPServer(createDNSLookupContext(), domainName, site, preferredServer); } public List<SocketInfo> obtainLDAPServer(ActiveDirectoryDomain activeDirectoryDomain) throws NamingException { return obtainLDAPServer(createDNSLookupContext(), activeDirectoryDomain.getName(), activeDirectoryDomain.getSite(), activeDirectoryDomain.getServers()); } // domain name prefixes // see http://technet.microsoft.com/en-us/library/cc759550(WS.10).aspx private static final List<String> CANDIDATES = Arrays.asList("_gc._tcp.", "_ldap._tcp."); /** * Use DNS and obtains the LDAP servers that we should try. * * @param preferredServers * If non-null, these servers are reported instead of doing the discovery. * In previous versions, this was simply added on top of the auto-discovered list, but this option * is useful when you have many domain controllers (because a single mistyped password can cause * an authentication attempt with every listed server, which can lock the user out!) This also * puts this feature in alignment with {@link #DOMAIN_CONTROLLERS}, which seems to indicate that * there are users who prefer this behaviour. * * @return A list with at least one item. */ public List<SocketInfo> obtainLDAPServer(DirContext ictx, String domainName, String site, String preferredServers) throws NamingException { List<SocketInfo> result = new ArrayList<SocketInfo>(); if (preferredServers==null || preferredServers.isEmpty()) preferredServers = DOMAIN_CONTROLLERS; if (preferredServers!=null) { for (String token : preferredServers.split(",")) { result.add(new SocketInfo(token.trim())); } return result; } String ldapServer = null; Attribute a = null; NamingException failure = null; // try global catalog if it exists first, then the particular domain for (String candidate : CANDIDATES) { ldapServer = candidate+(site!=null ? site+"._sites." : "")+domainName; LOGGER.fine("Attempting to resolve "+ldapServer+" to SRV record"); try { Attributes attributes = ictx.getAttributes(ldapServer, new String[] { "SRV" }); a = attributes.get("SRV"); if (a!=null) break; } catch (NamingException e) { // failed retrieval. try next option. failure = e; } catch (NumberFormatException x) { failure = (NamingException) new NamingException("JDK IPv6 bug encountered").initCause(x); } } if (a!=null) { // discover servers class PrioritizedSocketInfo implements Comparable<PrioritizedSocketInfo> { SocketInfo socket; int priority; PrioritizedSocketInfo(SocketInfo socket, int priority) { this.socket = socket; this.priority = priority; } @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "Weird and unpredictable behaviour intentional for load balancing.") public int compareTo(PrioritizedSocketInfo that) { return that.priority - this.priority; // sort them so that bigger priority comes first } } List<PrioritizedSocketInfo> plist = new ArrayList<PrioritizedSocketInfo>(); for (NamingEnumeration ne = a.getAll(); ne.hasMoreElements();) { String record = ne.next().toString(); LOGGER.fine("SRV record found: "+record); String[] fields = record.split(" "); // fields[1]: weight // fields[2]: port // fields[3]: target host name String hostName = fields[3]; // cut off trailing ".". JENKINS-2647 if (hostName.endsWith(".")) hostName = hostName.substring(0, hostName.length()-1); int port = Integer.parseInt(fields[2]); if (FORCE_LDAPS) { // map to LDAPS ports. I don't think there's any SRV records specifically for LDAPS. // I think Microsoft considers LDAP+TLS the way to go, or else there should have been // separate SRV entries. if (port==389) port=636; if (port==3268) port=3269; } int p = Integer.parseInt(fields[0]); plist.add(new PrioritizedSocketInfo(new SocketInfo(hostName, port),p)); } Collections.sort(plist); for (PrioritizedSocketInfo psi : plist) result.add(psi.socket); } if (result.isEmpty()) { NamingException x = new NamingException("No SRV record found for " + ldapServer); if (failure!=null) x.initCause(failure); throw x; } LOGGER.fine(ldapServer + " resolved to " + result); return result; } } @Override public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException { return getAuthenticationProvider().loadGroupByGroupname(groupname); } /** * Interface that actually talks to Active Directory. */ public AbstractActiveDirectoryAuthenticationProvider getAuthenticationProvider() { return (AbstractActiveDirectoryAuthenticationProvider)getSecurityComponents().userDetails; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { // delegate to one of our ActiveDirectory(Unix)?AuthenticationProvider return getAuthenticationProvider().loadUserByUsername(username); } @Override protected UserDetails authenticate(String username, String password) throws AuthenticationException { return getAuthenticationProvider().retrieveUser(username,new UsernamePasswordAuthenticationToken(username,password)); } private static final Logger LOGGER = Logger.getLogger(ActiveDirectorySecurityRealm.class.getName()); /** * If non-null, this value specifies the domain controllers and overrides all the lookups. * * The format is "host:port,host:port,..." * * @deprecated as of 1.28 * Use the UI field. */ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Diagnostic fields are left mutable so that groovy console can be used to dynamically turn/off probes.") public static String DOMAIN_CONTROLLERS = System.getProperty(ActiveDirectorySecurityRealm.class.getName()+".domainControllers"); /** * Instead of LDAP+TLS upgrade, start right away with LDAPS. * For the time being I'm trying not to expose this to users. I don't see why any AD shouldn't support * TLS upgrade if it's got the certificate. * * One legitimate use case is when the domain controller is Windows 2000, which doesn't support TLS * (according to http://support.microsoft.com/kb/321051). */ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "Diagnostic fields are left mutable so that groovy console can be used to dynamically turn/off probes.") public static boolean FORCE_LDAPS = Boolean.getBoolean(ActiveDirectorySecurityRealm.class.getName()+".forceLdaps"); /** * Store all the extra environment variable to be used on the LDAP Context */ public static class EnvironmentProperty extends AbstractDescribableImpl<EnvironmentProperty> implements Serializable { private final String name; private final String value; @DataBoundConstructor public EnvironmentProperty(String name, String value) { this.name = name; this.value = value; } public String getName() { return name; } public String getValue() { return value; } public static Map<String,String> toMap(List<EnvironmentProperty> properties) { final Map<String, String> result = new LinkedHashMap<String, String>(); if (properties != null) { for (EnvironmentProperty property:properties) { result.put(property.getName(), property.getValue()); } return result; } return result; } @Extension public static class DescriptorImpl extends Descriptor<EnvironmentProperty> { @Override public String getDisplayName() { return null; } } } @Extension public final static TlsConfigurationAdministrativeMonitor NOTICE = new TlsConfigurationAdministrativeMonitor(); /** * Administrative Monitor for changing TLS certificates management */ public static final class TlsConfigurationAdministrativeMonitor extends AdministrativeMonitor { public boolean isActivated() { SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm(); if (securityRealm instanceof ActiveDirectorySecurityRealm) { ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm; if (activeDirectorySecurityRealm.tlsConfiguration == null) { return true; } } return false; } /** * Depending on whether the user said "dismiss" or "correct", send him to the right place. */ public void doAct(StaplerRequest req, StaplerResponse rsp) throws IOException { if(req.hasParameter("correct")) { rsp.sendRedirect(req.getRootPath()+"/configureSecurity"); } } public static TlsConfigurationAdministrativeMonitor get() { return AdministrativeMonitor.all().get(TlsConfigurationAdministrativeMonitor.class); } } }