Skip to content

Commit

Permalink
[FIXED JENKINS-41744] ManagementLink to improve the supportability of…
Browse files Browse the repository at this point in the history
… the AD plugin (#59)

[JENKINS-41744] ManagementLink to improve the supportability of the AD plugin
  • Loading branch information
fbelzunc committed Jun 14, 2017
1 parent 6438b3d commit 4e46466
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 29 deletions.
Expand Up @@ -39,13 +39,16 @@
import org.kohsuke.stapler.QueryParameter;

import javax.naming.CommunicationException;
import javax.naming.Context;
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.servlet.ServletException;
import java.io.IOException;
import java.io.Serializable;
import java.util.Hashtable;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
Expand Down Expand Up @@ -101,6 +104,23 @@ public class ActiveDirectoryDomain extends AbstractDescribableImpl<ActiveDirecto

public Secret bindPassword;

// domain name prefixes
// see http://technet.microsoft.com/en-us/library/cc759550(WS.10).aspx
public enum Catalog {
GC("_gc._tcp."),
LDAP("_ldap._tcp.");

private final String name;

Catalog(String s) {
name = s;
}

public String toString() {
return this.name;
}
}

public ActiveDirectoryDomain(String name, String servers) {
this(name, servers, null, null, null);
}
Expand Down Expand Up @@ -150,6 +170,66 @@ public String getSite() {
return site;
}

/**
* Get the record from a domain
*
* @return the record of a domain
*/
public Attribute getRecordFromDomain(){
DirContext ictx;
Attribute a = null;
try {
LOGGER.log(Level.FINE, "Attempting to resolve {0} to NS record", name);
ictx = createDNSLookupContext();
Attributes attributes = ictx.getAttributes(name, new String[]{"NS"});
a = attributes.get("NS");
if (a == null) {
LOGGER.log(Level.FINE, "Attempting to resolve {0} to A record", name);
attributes = ictx.getAttributes(name, new String[]{"A"});
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, a});
} catch (NamingException e) {
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s to A record", name), e);
}
return a;
}

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

/**
* Get the list of servers which compose the {@link Catalog}
*
* The {@link Catalog} can be gc or ldap.
*
* @return the list of servers in the selected {@link Catalog}
*/
public Attribute getServersOnCatalog(String catalog) {
catalog = Catalog.valueOf(catalog).toString();
String ldapServer = catalog + (site != null ? site + "._sites." : "") + this.name;
LOGGER.log(Level.FINE, "Attempting to resolve {0} to SRV record", ldapServer);
try {
Attributes attributes = createDNSLookupContext().getAttributes(ldapServer, new String[] { "SRV" });
return attributes.get("SRV");
} catch (NamingException e) {
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s", ldapServer), e);
} catch (NumberFormatException x) {
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s", ldapServer), x);
}
return null;
}

/**
* Convert empty string to null.
*/
Expand Down Expand Up @@ -195,27 +275,17 @@ public FormValidation doValidateTest(@QueryParameter(fixEmpty = true) String nam
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");
}
List<ActiveDirectoryDomain> activeDirectoryDomains = activeDirectorySecurityRealm.getDomains();

// There should be only one domain as the fake domain only contains one
for (ActiveDirectoryDomain activeDirectoryDomain : activeDirectoryDomains) {
if (activeDirectoryDomain.getRecordFromDomain() != null) {
return FormValidation.error(name + " doesn't look like a valid 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
DirContext ictx = activeDirectorySecurityRealm.getDescriptor().createDNSLookupContext();
List<SocketInfo> obtainerServers;
try {
obtainerServers = activeDirectorySecurityRealm.getDescriptor().obtainLDAPServer(ictx, name, site, servers);
Expand Down
Expand Up @@ -669,7 +669,7 @@ private LdapContext bind(String principalName, String password, SocketInfo serve
Thread.currentThread().setName(oldName);
}
}

/**
* Creates {@link DirContext} for accesssing DNS.
*/
Expand All @@ -689,10 +689,6 @@ public List<SocketInfo> obtainLDAPServer(ActiveDirectoryDomain activeDirectoryDo
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.
*
Expand Down Expand Up @@ -724,9 +720,9 @@ public List<SocketInfo> obtainLDAPServer(DirContext ictx, String domainName, Str
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");
for (ActiveDirectoryDomain.Catalog catalog : ActiveDirectoryDomain.Catalog.values()) {
ldapServer = catalog + (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");
Expand Down
@@ -0,0 +1,209 @@
package hudson.plugins.active_directory;

/*
* The MIT License
*
* Copyright (c) 2017, 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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import hudson.model.ManagementLink;
import hudson.security.SecurityRealm;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.util.ProgressiveRendering;
import net.sf.json.JSON;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.acegisecurity.userdetails.UserDetails;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
* ManagementLink to provide an Active Directory health status
*
* Intend to report a health status of the Active Directory Domain through
* a ManagementLink on Jenkins.
* - Check if there is any broken Domain Controller on the farm
* - Report the connection time
* - Provides the User lookup time
*
* @since 2.1
*/
@Extension
public class ActiveDirectoryStatus extends ManagementLink {

@Override
public String getIconFileName() {
return "/plugin/active-directory/images/icon.png";
}

@Override
public String getDisplayName() {
return Messages._ActiveDirectoryStatus_ActiveDirectoryHealthStatus().toString();
}

@Override
public String getUrlName() {
return "ad-health";
}

/**
* Get the list of domains configured on the Security Realm
*
* @return the Active Directory domains {@link ActiveDirectoryDomain}.
*/
@Restricted(NoExternalUse.class)
public static List<ActiveDirectoryDomain> getDomains() {
SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm();
if (securityRealm instanceof ActiveDirectorySecurityRealm) {
ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm;
return activeDirectorySecurityRealm.getDomains();
}
return Collections.emptyList();
}

/**
* Start the Domain Controller Health checks against a specific domain
*
* @param domain to check the health
* @return {@link ProgressiveRendering}
*/
@Restricted(NoExternalUse.class)
public ProgressiveRendering startDomainHealthChecks(final String domain) {
return new ProgressiveRendering() {
final List<ServerHealth> domainHealth = new LinkedList<ServerHealth>();
@Override protected void compute() throws Exception {
for (ActiveDirectoryDomain domainItem : getDomains()) {
if (canceled()) {
return;
}
if (domainItem.getName().equals(domain)) {
SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm();
if (securityRealm instanceof ActiveDirectorySecurityRealm) {
ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm;
List<SocketInfo> servers = activeDirectorySecurityRealm.getDescriptor().obtainLDAPServer(domainItem);
for (SocketInfo socketInfo : servers) {
ServerHealth serverHealth = new ServerHealth(socketInfo);
domainHealth.add(serverHealth);
}
}
}
}
}
@Override protected synchronized JSON data() {
JSONArray r = new JSONArray();
for (ServerHealth serverHealth : domainHealth) {
r.add(serverHealth);
}
domainHealth.clear();
return new JSONObject().accumulate("domainHealth", r);
}
};
}

@Restricted(NoExternalUse.class)
public ListBoxModel doFillDomainsItems() {
ListBoxModel model = new ListBoxModel();
for (ActiveDirectoryDomain domain : getDomains()) {
model.add(domain.getName());
}
return model;
}

/**
* ServerHealth of a SocketInfo
*/
@SuppressFBWarnings("UUF_UNUSED_FIELD")
public static class ServerHealth extends SocketInfo {
/**
* true if able to retrieve the user details from Jenkins
*/
private boolean canLogin;

/**
* Time for a Socket to reach out the target server
*/
private long pingExecutionTime;

/**
* Total amount of time for Jenkins to perform SecurityRealm.loadUserByUsername
*/
private long loginExecutionTime;

public ServerHealth(SocketInfo socketInfo) {
super(socketInfo.getHost(), socketInfo.getPort());
this.pingExecutionTime = this.computePingExecutionTime();
this.loginExecutionTime = this.computeLoginExecutionTime();
}

@Restricted(NoExternalUse.class)
public boolean isCanLogin() {
return true ? loginExecutionTime != -1 : false;
}

@Restricted(NoExternalUse.class)
public long getPingExecutionTime() {
return pingExecutionTime;
}

@Restricted(NoExternalUse.class)
public long getLoginExecutionTime() {
return loginExecutionTime;
}

/**
* Retrieve the time for Jenkins to perform SecurityRealm.loadUserByUsername
*
* @return -1 in case the user could not be retrieved
*/
private long computeLoginExecutionTime() {
String username = Jenkins.getAuthentication().getName();
long t0 = System.currentTimeMillis();
UserDetails userDetails = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(username);
long t1 = System.currentTimeMillis();
return (userDetails!=null) ? (t1 - t0) : -1;
}

/**
* Retrieve the time to to establish a Socket connection with the AD server
*
* @return -1 in case the connection failed
*/
private long computePingExecutionTime() {
try {
long t0 = System.currentTimeMillis();
super.connect().close();
long t1 = System.currentTimeMillis();
return t1-t0;
} catch (IOException e) {
}
return -1;
}
}

}

0 comments on commit 4e46466

Please sign in to comment.