package hudson.plugins.active_directory; /* * The MIT License * * Copyright (c) 2016, Felix 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 hudson.Extension; import hudson.Functions; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.util.FormValidation; import hudson.util.Secret; import org.acegisecurity.BadCredentialsException; import org.apache.commons.lang.StringUtils; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import javax.naming.CommunicationException; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.servlet.ServletException; import java.io.IOException; import java.io.Serializable; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import static hudson.plugins.active_directory.ActiveDirectoryUnixAuthenticationProvider.toDC; /** * Represents an Active Directory domain with its Domain Controllers * * Easily allows you to match Domains with Domains Controllers * * @since 2.0 */ public class ActiveDirectoryDomain extends AbstractDescribableImpl<ActiveDirectoryDomain> implements Serializable { /** * Domain name * * <p> * When this plugin is used on Windows, this field is null, and we use ADSI * and ADO through com4j to perform authentication. * * <p> * OTOH, when this plugin runs on non-Windows, this field must be non-null, * and we'll use LDAP for authentication. */ public String name; /** * If non-null, Jenkins will try to connect at this server at the first priority, before falling back to * discovered DNS servers. */ public String servers; /** * 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. */ public String site; /** * If non-null, use this name and password to bind to LDAP to obtain the DN * of the user trying to login. This is unnecessary in a single-domain mode, * where we can just bind with the user name and password provided during * the login, but in a forest mode, without some known credential, we cannot * figure out which domain in the forest the user belongs to. */ public String bindName; public Secret bindPassword; public ActiveDirectoryDomain(String name, String servers) { this(name, servers, null, null, null); } @DataBoundConstructor public ActiveDirectoryDomain(String name, String servers, String site, String bindName, String bindPassword) { this.name = name; // Append default port if not specified servers = fixEmpty(servers); if (servers != null) { String[] serversArray = servers.split(","); for (int i = 0; i < serversArray.length; i++) { if (!serversArray[i].contains(":")) { serversArray[i] += ":3268"; } } servers = StringUtils.join(serversArray, ","); } this.servers = servers; this.site = fixEmpty(site); this.bindName = fixEmpty(bindName); this.bindPassword = Secret.fromString(fixEmpty(bindPassword)); } @Restricted(NoExternalUse.class) public String getName() { return name; } @Restricted(NoExternalUse.class) public String getServers() { return servers; } @Restricted(NoExternalUse.class) public String getBindName() { return bindName; } @Restricted(NoExternalUse.class) public Secret getBindPassword() { return bindPassword; } @Restricted(NoExternalUse.class) public String getSite() { return site; } /** * Convert empty string to null. */ public static String fixEmpty(String s) { if(s==null || s.length()==0) return null; return s; } @Extension public static class DescriptorImpl extends Descriptor<ActiveDirectoryDomain> { @Override public String getDisplayName() { return ""; } public FormValidation doValidateTest(@QueryParameter(fixEmpty = true) String name, @QueryParameter(fixEmpty = true) String servers, @QueryParameter(fixEmpty = true) String site, @QueryParameter(fixEmpty = true) String bindName, @QueryParameter(fixEmpty = true) String bindPassword) throws IOException, ServletException, NamingException { // Create a fake ActiveDirectorySecurityRealm ActiveDirectorySecurityRealm activeDirectorySecurityRealm = new ActiveDirectorySecurityRealm(name, site, bindName, bindPassword, servers); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); try { Functions.checkPermission(Hudson.ADMINISTER); // In case we can do native authentication if (activeDirectorySecurityRealm.getDescriptor().canDoNativeAuth() && name==null) { // this check must be identical to that of ActiveDirectory.groovy try { // make sure we can connect via ADSI new ActiveDirectoryAuthenticationProvider(); return FormValidation.ok("Success"); } catch (Exception e) { return FormValidation.error(e, "Failed to contact Active Directory"); } } // If non nativate authentication then check there is at least one Domain created in the UI if (name==null || name.isEmpty()) { return FormValidation.error("No domain was set"); } Secret password = Secret.fromString(bindPassword); if (bindName!=null && password==null) return FormValidation.error("Bind DN is specified but not the password"); DirContext ictx; // First test the sanity of the domain name itself try { LOGGER.log(Level.FINE, "Attempting to resolve {0} to NS record", name); ictx = activeDirectorySecurityRealm.getDescriptor().createDNSLookupContext(); Attributes attributes = ictx.getAttributes(name, new String[]{"NS"}); Attribute ns = attributes.get("NS"); if (ns == null) { LOGGER.log(Level.FINE, "Attempting to resolve {0} to A record", name); attributes = ictx.getAttributes(name, new String[]{"A"}); Attribute a = attributes.get("A"); if (a == null) { throw new NamingException(name + " doesn't look like a domain name"); } } LOGGER.log(Level.FINE, "{0} resolved to {1}", new Object[]{name, ns}); } catch (NamingException e) { LOGGER.log(Level.WARNING, String.format("Failed to resolve %s to A record", name), e); return FormValidation.error(e, name + " doesn't look like a valid domain name"); } // Then look for the LDAP server List<SocketInfo> obtainerServers; try { obtainerServers = activeDirectorySecurityRealm.getDescriptor().obtainLDAPServer(ictx, name, site, servers); } catch (NamingException e) { String msg = site == null ? "No LDAP server was found in " + name : "No LDAP server was found in the " + site + " site of " + name; LOGGER.log(Level.WARNING, msg, e); return FormValidation.error(e, msg); } if (bindName != null) { // Make sure the bind actually works try { DirContext context = activeDirectorySecurityRealm.getDescriptor().bind(bindName, Secret.toString(password), obtainerServers); try { // Actually do a search to make sure the credential is valid Attributes userAttributes = new LDAPSearchBuilder(context, toDC(name)).subTreeScope().searchOne("(objectClass=user)"); if (userAttributes == null) { return FormValidation.error(Messages.ActiveDirectorySecurityRealm_NoUsers()); } } finally { context.close(); } } catch (BadCredentialsException e) { Throwable t = e.getCause(); if (t instanceof CommunicationException) { return FormValidation.error(e, "Any Domain Controller is reachable"); } return FormValidation.error(e, "Bad bind username or password"); } catch (javax.naming.AuthenticationException e) { return FormValidation.error(e, "Bad bind username or password"); } catch (Exception e) { return FormValidation.error(e, e.getMessage()); } } else { // just some connection test // try to connect to LDAP port to make sure this machine has LDAP service IOException error = null; for (SocketInfo si : obtainerServers) { try { si.connect().close(); break; // looks good } catch (IOException e) { LOGGER.log(Level.FINE, String.format("Failed to connect to %s", si), e); error = e; // try the next server in the list } } if (error != null) { LOGGER.log(Level.WARNING, String.format("Failed to connect to %s", servers), error); return FormValidation.error(error, "Failed to connect to " + servers); } } // looks good return FormValidation.ok("Success"); } finally { Thread.currentThread().setContextClassLoader(ccl); } } } private static final Logger LOGGER = Logger.getLogger(ActiveDirectoryUnixAuthenticationProvider.class.getName()); }