Skip to content

Commit 4e46466

Browse files
authored
[FIXED JENKINS-41744] ManagementLink to improve the supportability of the AD plugin (#59)
[JENKINS-41744] ManagementLink to improve the supportability of the AD plugin
1 parent 6438b3d commit 4e46466

File tree

8 files changed

+470
-29
lines changed

8 files changed

+470
-29
lines changed

src/main/java/hudson/plugins/active_directory/ActiveDirectoryDomain.java

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,16 @@
3939
import org.kohsuke.stapler.QueryParameter;
4040

4141
import javax.naming.CommunicationException;
42+
import javax.naming.Context;
4243
import javax.naming.NamingException;
4344
import javax.naming.directory.Attribute;
4445
import javax.naming.directory.Attributes;
4546
import javax.naming.directory.DirContext;
47+
import javax.naming.directory.InitialDirContext;
4648
import javax.servlet.ServletException;
4749
import java.io.IOException;
4850
import java.io.Serializable;
51+
import java.util.Hashtable;
4952
import java.util.List;
5053
import java.util.logging.Level;
5154
import java.util.logging.Logger;
@@ -101,6 +104,23 @@ public class ActiveDirectoryDomain extends AbstractDescribableImpl<ActiveDirecto
101104

102105
public Secret bindPassword;
103106

107+
// domain name prefixes
108+
// see http://technet.microsoft.com/en-us/library/cc759550(WS.10).aspx
109+
public enum Catalog {
110+
GC("_gc._tcp."),
111+
LDAP("_ldap._tcp.");
112+
113+
private final String name;
114+
115+
Catalog(String s) {
116+
name = s;
117+
}
118+
119+
public String toString() {
120+
return this.name;
121+
}
122+
}
123+
104124
public ActiveDirectoryDomain(String name, String servers) {
105125
this(name, servers, null, null, null);
106126
}
@@ -150,6 +170,66 @@ public String getSite() {
150170
return site;
151171
}
152172

173+
/**
174+
* Get the record from a domain
175+
*
176+
* @return the record of a domain
177+
*/
178+
public Attribute getRecordFromDomain(){
179+
DirContext ictx;
180+
Attribute a = null;
181+
try {
182+
LOGGER.log(Level.FINE, "Attempting to resolve {0} to NS record", name);
183+
ictx = createDNSLookupContext();
184+
Attributes attributes = ictx.getAttributes(name, new String[]{"NS"});
185+
a = attributes.get("NS");
186+
if (a == null) {
187+
LOGGER.log(Level.FINE, "Attempting to resolve {0} to A record", name);
188+
attributes = ictx.getAttributes(name, new String[]{"A"});
189+
a = attributes.get("A");
190+
if (a == null) {
191+
throw new NamingException(name + " doesn't look like a domain name");
192+
}
193+
}
194+
LOGGER.log(Level.FINE, "{0} resolved to {1}", new Object[]{name, a});
195+
} catch (NamingException e) {
196+
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s to A record", name), e);
197+
}
198+
return a;
199+
}
200+
201+
/**
202+
* Creates {@link DirContext} for accesssing DNS.
203+
*/
204+
public DirContext createDNSLookupContext() throws NamingException {
205+
Hashtable env = new Hashtable();
206+
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
207+
env.put("java.naming.provider.url", "dns:");
208+
return new InitialDirContext(env);
209+
}
210+
211+
/**
212+
* Get the list of servers which compose the {@link Catalog}
213+
*
214+
* The {@link Catalog} can be gc or ldap.
215+
*
216+
* @return the list of servers in the selected {@link Catalog}
217+
*/
218+
public Attribute getServersOnCatalog(String catalog) {
219+
catalog = Catalog.valueOf(catalog).toString();
220+
String ldapServer = catalog + (site != null ? site + "._sites." : "") + this.name;
221+
LOGGER.log(Level.FINE, "Attempting to resolve {0} to SRV record", ldapServer);
222+
try {
223+
Attributes attributes = createDNSLookupContext().getAttributes(ldapServer, new String[] { "SRV" });
224+
return attributes.get("SRV");
225+
} catch (NamingException e) {
226+
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s", ldapServer), e);
227+
} catch (NumberFormatException x) {
228+
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s", ldapServer), x);
229+
}
230+
return null;
231+
}
232+
153233
/**
154234
* Convert empty string to null.
155235
*/
@@ -195,27 +275,17 @@ public FormValidation doValidateTest(@QueryParameter(fixEmpty = true) String nam
195275
if (bindName!=null && password==null)
196276
return FormValidation.error("Bind DN is specified but not the password");
197277

198-
DirContext ictx;
199278
// First test the sanity of the domain name itself
200-
try {
201-
LOGGER.log(Level.FINE, "Attempting to resolve {0} to NS record", name);
202-
ictx = activeDirectorySecurityRealm.getDescriptor().createDNSLookupContext();
203-
Attributes attributes = ictx.getAttributes(name, new String[]{"NS"});
204-
Attribute ns = attributes.get("NS");
205-
if (ns == null) {
206-
LOGGER.log(Level.FINE, "Attempting to resolve {0} to A record", name);
207-
attributes = ictx.getAttributes(name, new String[]{"A"});
208-
Attribute a = attributes.get("A");
209-
if (a == null) {
210-
throw new NamingException(name + " doesn't look like a domain name");
211-
}
279+
List<ActiveDirectoryDomain> activeDirectoryDomains = activeDirectorySecurityRealm.getDomains();
280+
281+
// There should be only one domain as the fake domain only contains one
282+
for (ActiveDirectoryDomain activeDirectoryDomain : activeDirectoryDomains) {
283+
if (activeDirectoryDomain.getRecordFromDomain() != null) {
284+
return FormValidation.error(name + " doesn't look like a valid domain name");
212285
}
213-
LOGGER.log(Level.FINE, "{0} resolved to {1}", new Object[]{name, ns});
214-
} catch (NamingException e) {
215-
LOGGER.log(Level.WARNING, String.format("Failed to resolve %s to A record", name), e);
216-
return FormValidation.error(e, name + " doesn't look like a valid domain name");
217286
}
218287
// Then look for the LDAP server
288+
DirContext ictx = activeDirectorySecurityRealm.getDescriptor().createDNSLookupContext();
219289
List<SocketInfo> obtainerServers;
220290
try {
221291
obtainerServers = activeDirectorySecurityRealm.getDescriptor().obtainLDAPServer(ictx, name, site, servers);

src/main/java/hudson/plugins/active_directory/ActiveDirectorySecurityRealm.java

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ private LdapContext bind(String principalName, String password, SocketInfo serve
669669
Thread.currentThread().setName(oldName);
670670
}
671671
}
672-
672+
673673
/**
674674
* Creates {@link DirContext} for accesssing DNS.
675675
*/
@@ -689,10 +689,6 @@ public List<SocketInfo> obtainLDAPServer(ActiveDirectoryDomain activeDirectoryDo
689689
return obtainLDAPServer(createDNSLookupContext(), activeDirectoryDomain.getName(), activeDirectoryDomain.getSite(), activeDirectoryDomain.getServers());
690690
}
691691

692-
// domain name prefixes
693-
// see http://technet.microsoft.com/en-us/library/cc759550(WS.10).aspx
694-
private static final List<String> CANDIDATES = Arrays.asList("_gc._tcp.", "_ldap._tcp.");
695-
696692
/**
697693
* Use DNS and obtains the LDAP servers that we should try.
698694
*
@@ -724,9 +720,9 @@ public List<SocketInfo> obtainLDAPServer(DirContext ictx, String domainName, Str
724720
NamingException failure = null;
725721

726722
// try global catalog if it exists first, then the particular domain
727-
for (String candidate : CANDIDATES) {
728-
ldapServer = candidate+(site!=null ? site+"._sites." : "")+domainName;
729-
LOGGER.fine("Attempting to resolve "+ldapServer+" to SRV record");
723+
for (ActiveDirectoryDomain.Catalog catalog : ActiveDirectoryDomain.Catalog.values()) {
724+
ldapServer = catalog + (site!=null ? site + "._sites." : "") + domainName;
725+
LOGGER.fine("Attempting to resolve " + ldapServer + " to SRV record");
730726
try {
731727
Attributes attributes = ictx.getAttributes(ldapServer, new String[] { "SRV" });
732728
a = attributes.get("SRV");
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package hudson.plugins.active_directory;
2+
3+
/*
4+
* The MIT License
5+
*
6+
* Copyright (c) 2017, Felix Belzunce Arcos, CloudBees, Inc., and contributors
7+
*
8+
* Permission is hereby granted, free of charge, to any person obtaining a copy
9+
* of this software and associated documentation files (the "Software"), to deal
10+
* in the Software without restriction, including without limitation the rights
11+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
* copies of the Software, and to permit persons to whom the Software is
13+
* furnished to do so, subject to the following conditions:
14+
*
15+
* The above copyright notice and this permission notice shall be included in
16+
* all copies or substantial portions of the Software.
17+
*
18+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24+
* THE SOFTWARE.
25+
*/
26+
27+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
28+
import hudson.Extension;
29+
import hudson.model.ManagementLink;
30+
import hudson.security.SecurityRealm;
31+
import hudson.util.ListBoxModel;
32+
import jenkins.model.Jenkins;
33+
import jenkins.util.ProgressiveRendering;
34+
import net.sf.json.JSON;
35+
import net.sf.json.JSONArray;
36+
import net.sf.json.JSONObject;
37+
import org.acegisecurity.userdetails.UserDetails;
38+
import org.kohsuke.accmod.Restricted;
39+
import org.kohsuke.accmod.restrictions.NoExternalUse;
40+
41+
import java.io.IOException;
42+
import java.util.Collections;
43+
import java.util.LinkedList;
44+
import java.util.List;
45+
46+
/**
47+
* ManagementLink to provide an Active Directory health status
48+
*
49+
* Intend to report a health status of the Active Directory Domain through
50+
* a ManagementLink on Jenkins.
51+
* - Check if there is any broken Domain Controller on the farm
52+
* - Report the connection time
53+
* - Provides the User lookup time
54+
*
55+
* @since 2.1
56+
*/
57+
@Extension
58+
public class ActiveDirectoryStatus extends ManagementLink {
59+
60+
@Override
61+
public String getIconFileName() {
62+
return "/plugin/active-directory/images/icon.png";
63+
}
64+
65+
@Override
66+
public String getDisplayName() {
67+
return Messages._ActiveDirectoryStatus_ActiveDirectoryHealthStatus().toString();
68+
}
69+
70+
@Override
71+
public String getUrlName() {
72+
return "ad-health";
73+
}
74+
75+
/**
76+
* Get the list of domains configured on the Security Realm
77+
*
78+
* @return the Active Directory domains {@link ActiveDirectoryDomain}.
79+
*/
80+
@Restricted(NoExternalUse.class)
81+
public static List<ActiveDirectoryDomain> getDomains() {
82+
SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm();
83+
if (securityRealm instanceof ActiveDirectorySecurityRealm) {
84+
ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm;
85+
return activeDirectorySecurityRealm.getDomains();
86+
}
87+
return Collections.emptyList();
88+
}
89+
90+
/**
91+
* Start the Domain Controller Health checks against a specific domain
92+
*
93+
* @param domain to check the health
94+
* @return {@link ProgressiveRendering}
95+
*/
96+
@Restricted(NoExternalUse.class)
97+
public ProgressiveRendering startDomainHealthChecks(final String domain) {
98+
return new ProgressiveRendering() {
99+
final List<ServerHealth> domainHealth = new LinkedList<ServerHealth>();
100+
@Override protected void compute() throws Exception {
101+
for (ActiveDirectoryDomain domainItem : getDomains()) {
102+
if (canceled()) {
103+
return;
104+
}
105+
if (domainItem.getName().equals(domain)) {
106+
SecurityRealm securityRealm = Jenkins.getInstance().getSecurityRealm();
107+
if (securityRealm instanceof ActiveDirectorySecurityRealm) {
108+
ActiveDirectorySecurityRealm activeDirectorySecurityRealm = (ActiveDirectorySecurityRealm) securityRealm;
109+
List<SocketInfo> servers = activeDirectorySecurityRealm.getDescriptor().obtainLDAPServer(domainItem);
110+
for (SocketInfo socketInfo : servers) {
111+
ServerHealth serverHealth = new ServerHealth(socketInfo);
112+
domainHealth.add(serverHealth);
113+
}
114+
}
115+
}
116+
}
117+
}
118+
@Override protected synchronized JSON data() {
119+
JSONArray r = new JSONArray();
120+
for (ServerHealth serverHealth : domainHealth) {
121+
r.add(serverHealth);
122+
}
123+
domainHealth.clear();
124+
return new JSONObject().accumulate("domainHealth", r);
125+
}
126+
};
127+
}
128+
129+
@Restricted(NoExternalUse.class)
130+
public ListBoxModel doFillDomainsItems() {
131+
ListBoxModel model = new ListBoxModel();
132+
for (ActiveDirectoryDomain domain : getDomains()) {
133+
model.add(domain.getName());
134+
}
135+
return model;
136+
}
137+
138+
/**
139+
* ServerHealth of a SocketInfo
140+
*/
141+
@SuppressFBWarnings("UUF_UNUSED_FIELD")
142+
public static class ServerHealth extends SocketInfo {
143+
/**
144+
* true if able to retrieve the user details from Jenkins
145+
*/
146+
private boolean canLogin;
147+
148+
/**
149+
* Time for a Socket to reach out the target server
150+
*/
151+
private long pingExecutionTime;
152+
153+
/**
154+
* Total amount of time for Jenkins to perform SecurityRealm.loadUserByUsername
155+
*/
156+
private long loginExecutionTime;
157+
158+
public ServerHealth(SocketInfo socketInfo) {
159+
super(socketInfo.getHost(), socketInfo.getPort());
160+
this.pingExecutionTime = this.computePingExecutionTime();
161+
this.loginExecutionTime = this.computeLoginExecutionTime();
162+
}
163+
164+
@Restricted(NoExternalUse.class)
165+
public boolean isCanLogin() {
166+
return true ? loginExecutionTime != -1 : false;
167+
}
168+
169+
@Restricted(NoExternalUse.class)
170+
public long getPingExecutionTime() {
171+
return pingExecutionTime;
172+
}
173+
174+
@Restricted(NoExternalUse.class)
175+
public long getLoginExecutionTime() {
176+
return loginExecutionTime;
177+
}
178+
179+
/**
180+
* Retrieve the time for Jenkins to perform SecurityRealm.loadUserByUsername
181+
*
182+
* @return -1 in case the user could not be retrieved
183+
*/
184+
private long computeLoginExecutionTime() {
185+
String username = Jenkins.getAuthentication().getName();
186+
long t0 = System.currentTimeMillis();
187+
UserDetails userDetails = Jenkins.getInstance().getSecurityRealm().loadUserByUsername(username);
188+
long t1 = System.currentTimeMillis();
189+
return (userDetails!=null) ? (t1 - t0) : -1;
190+
}
191+
192+
/**
193+
* Retrieve the time to to establish a Socket connection with the AD server
194+
*
195+
* @return -1 in case the connection failed
196+
*/
197+
private long computePingExecutionTime() {
198+
try {
199+
long t0 = System.currentTimeMillis();
200+
super.connect().close();
201+
long t1 = System.currentTimeMillis();
202+
return t1-t0;
203+
} catch (IOException e) {
204+
}
205+
return -1;
206+
}
207+
}
208+
209+
}

0 commit comments

Comments
 (0)