View Javadoc
1   /*
2    * Copyright 2013–2024 Michael Osipov
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package net.sf.michaelo.tomcat.realm;
17  
18  import java.net.URI;
19  import java.net.URISyntaxException;
20  import java.security.Principal;
21  import java.security.cert.CertificateParsingException;
22  import java.security.cert.X509Certificate;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Base64;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.concurrent.atomic.AtomicLong;
34  
35  import javax.naming.CommunicationException;
36  import javax.naming.CompositeName;
37  import javax.naming.InvalidNameException;
38  import javax.naming.Name;
39  import javax.naming.NameParser;
40  import javax.naming.NamingEnumeration;
41  import javax.naming.NamingException;
42  import javax.naming.PartialResultException;
43  import javax.naming.ReferralException;
44  import javax.naming.ServiceUnavailableException;
45  import javax.naming.directory.Attribute;
46  import javax.naming.directory.Attributes;
47  import javax.naming.directory.DirContext;
48  import javax.naming.directory.SearchControls;
49  import javax.naming.directory.SearchResult;
50  import javax.naming.ldap.LdapName;
51  import javax.naming.ldap.ManageReferralControl;
52  import javax.naming.ldap.Rdn;
53  import javax.security.auth.x500.X500Principal;
54  
55  import net.sf.michaelo.dirctxsrc.DirContextSource;
56  import net.sf.michaelo.tomcat.authenticator.SpnegoAuthenticator;
57  import net.sf.michaelo.tomcat.realm.asn1.OtherNameAsn1Parser;
58  import net.sf.michaelo.tomcat.realm.asn1.OtherNameParseResult;
59  import net.sf.michaelo.tomcat.realm.mapper.SamAccountNameRfc2247Mapper;
60  import net.sf.michaelo.tomcat.realm.mapper.UserPrincipalNameSearchMapper;
61  import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper;
62  import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper.MappedValues;
63  
64  import org.apache.catalina.LifecycleException;
65  import org.apache.catalina.Server;
66  import org.apache.catalina.authenticator.SSLAuthenticator;
67  import org.apache.catalina.realm.CombinedRealm;
68  import org.apache.commons.lang3.StringUtils;
69  import org.apache.naming.ContextBindings;
70  import org.apache.tomcat.util.buf.Asn1Parser;
71  import org.apache.tomcat.util.collections.SynchronizedStack;
72  import org.ietf.jgss.GSSCredential;
73  import org.ietf.jgss.GSSException;
74  import org.ietf.jgss.GSSManager;
75  import org.ietf.jgss.GSSName;
76  import org.ietf.jgss.Oid;
77  
78  /**
79   * A realm which retrieves <em>already authenticated</em> users from Active Directory via LDAP.
80   *
81   * <h2 id="configuration">Configuration</h2> Following options can be configured:
82   * <ul>
83   * <li>{@code dirContextSourceName}: the name of the {@link DirContextSource} in JNDI with which
84   * principals will be retrieved.</li>
85   * <li>{@code localDirContextSource}: whether this {@code DirContextSource} is locally configured in
86   * the {@code context.xml} or globally configured in the {@code server.xml} (optional). Default
87   * value is {@code false}.</li>
88   * <li>{@code roleFormats}: comma-separated list of role formats to be applied to user security
89   * groups. The following values are possible: {@code sid} retrieves the {@code objectSid} and
90   * {@code sIDHistory} attribute values, {@code name} retrieves the {@code msDS-PrincipalName} attribute
91   * value representing the down-level logon name format: <code>{netbiosDomain}\{samAccountName}</code>,
92   * and {@code nameEx} retrieves the {@code distinguishedName} and {@code sAMAccountName} attribute
93   * values and converts the DC RDNs from the DN to the Kerberos realm and appends the
94   * {@code sAMAccountName} (reversed RFC 2247) with format <code>{realm}\{samAccountName}</code>.
95   * Default is {@code sid}.</li>
96   * <li>{@code prependRoleFormat}: whether the role format is prepended to the role as
97   * <code>{roleFormat}:{role}</code>. Default is {@code false}.
98   * <li>{@code additionalAttributes}: comma-separated list of attributes to be retrieved for the
99   * principal. Binary attributes must end with {@code ;binary} and will be stored as {@code byte[]},
100  * ordinary attributes will be stored as {@code String}. If an attribute is multivalued, it will be
101  * stored as {@code List}.</li>
102  * <li>{@code connectionPoolSize}: the maximum amount of directory server connections the pool will
103  * hold. Default is zero which means no connections will be pooled.</li>
104  * <li>{@code maxIdleTime}: the maximum amount of time in milliseconds a directory server connection
105  * should remain idle before it is closed. Default value is 15 minutes.</li>
106  * </ul>
107  * <h2>Connection Pooling</h2> This realm offers a poor man's directory server connection pooling
108  * which can drastically improve access performance for non-session (stateless) applications. It
109  * utilizes a LIFO structure based on {@link SynchronizedStack}. No background thread is managing
110  * the connections. They are acquired, validated, eventually closed and opened when
111  * {@link #getPrincipal(GSSName, GSSCredential)} is invoked. Validation involves a minimal and
112  * limited query with at most 500 ms of wait time just to verify the connection is alive and
113  * healthy. If this query fails, the connection is closed immediately. If the amount of requested
114  * connections exceeds the ones available in the pool, new ones are opened and pushed onto the pool.
115  * If the pool does not accept any addtional connections they are closed immediately.
116  * <p>
117  * <strong>Note:</strong> This connection pool feature has to be explicitly enabled by setting
118  * {@code connectionPoolSize} to greater than zero.
119  *
120  * <h2 id="on-usernames">On Usernames</h2>
121  * This realm processes supplied usernames with different types.
122  * <h3>Supported Types</h3> Only a subset of username types are accepted in contrast to
123  * other realm implementations. Namely, this realm must know what type is passed to properly map
124  * it into Active Directory search space with a {@link UsernameSearchMapper} implementation.
125  * The supported username types are:
126  * <ul>
127  * <li>{@link GSSName} by inspecting the string name type,</li>
128  * <li>{@link X509Certificate} by extracting the {@code SAN:otherName} field and matching for
129  * MS UPN type id (1.3.6.1.4.1.311.20.2.3).</li>
130  * </ul>
131  * <p>
132  * <strong>Note:</strong> Both types represent <em>already authenticated</em> users by means of a
133  * GSS and/or TLScontext.
134  *<h3>Canonicalization</h3>
135  * This realm will always try to canonicalize a given username type to a real {@link GSSName}
136  * with the string name type of {@code KRB5_NT_PRINCIPAL} (1.2.840.113554.1.2.2.1) similar to
137  * the {@code canonicalize} flag in the <a href="https://web.mit.edu/kerberos/krb5-1.19/doc/admin/conf_files/krb5_conf.html">
138  * {@code krb5.conf}</a> file. This makes the final {@link GSSName} fully usable in subsequent
139  * GSS-API calls.
140  *
141  * <h2 id="referral-handling">Referral Handling</h2> Active Directory uses two type of responses
142  * when it cannot complete a search request: referrals and search result references. Both are
143  * different in nature, read more about them <a href="https://documentation.avaya.com/en-US/bundle/DeployingAvayaDeviceServices_R8.1.4/page/LDAP_Search_Results_and_Referrals.html">here</a>.
144  * For this section I will use the term <i>referral</i> for both types synomously as does the
145  * <a href="https://docs.oracle.com/javase/jndi/tutorial/ldap/referral/jndi.html">JNDI/LDAP Provider documentation</a>.
146  * <br>
147  * When working with the default LDAP ports (not
148  * GC) or in a multi-forest environment, it is highly likely to receive referrals (either
149  * subordinate or cross) during a search or lookup. Sun's JNDI/LDAP Provider takes the following
150  * approach to handle referrals with the {@code java.naming.referral} property and its values:
151  * {@code ignore}, {@code throw}, and {@code follow}. You can ignore referrals altogether, but
152  * the provider will still signal a {@link PartialResultException} when a {@link NamingEnumeration}
153  * is iterated. The reason is because it adds a {@link ManageReferralControl} when {@code ignore}
154  * is set and assumes that the target server will ignore referrals, but this is a misconception
155  * in this provider implementation, see
156  * <a href="https://openldap-software.0penldap.narkive.com/cuImLMRw/managedsait#post2">here</a>
157  * and <a href="https://bugs.openjdk.java.net/browse/JDK-5109452">here</a>. It is also unclear
158  * whether Microsoft Active Directory supports this control.
159  * <br>
160  * This realm will catch this exception and continue to process the enumeration. If the {@code DirContextSource}
161  * is set to {@code throw}, this realm will catch the {@link ReferralException} also, but avoid
162  * following referrals manually (for several reasons) and will continue with the process.
163  * Following referrals automatically is a completely opaque operation to the application, no
164  * {@code ReferralException} is thrown, but the referrals are handled internally and referral
165  * contexts are queried and closed. If you choose to {@code follow} referrals you <strong>must</strong>
166  * use my <a href="https://michael-o.github.io/activedirectory-dns-locator/">Active Directory DNS Locator</a>
167  * otherwise the queries <strong>will</strong> fail and you will suffer from
168  * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8161361">JDK-8161361</a> and
169  * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8160768">JDK-8160768</a>!
170  * <p>
171  * <em>Why do you need to use my Active Directory DNS Locator?</em> Microsoft takes a very
172  * sophisticated approach on not to rely on hostnames because servers can be provisioned and
173  * decommissioned any time. Instead, they heavily rely on DNS domain names and DNS SRV records
174  * at runtime. I.e., an initial or a referral URL does not contain a hostname, but only a domain
175  * name. While you can connect to the service with this name, you cannot easily authenticate
176  * against it with Kerberos because one cannot bind the same SPN {@code ldap/<dnsDomainName>@<REALM>},
177  * e.g., {@code ldap/example.com@EXAMPLE.COM} to more than one account. If you try authenticate
178  * anyway, you will receive a "Server not found in Kerberos database (7)" error. Therefore, one has
179  * to perform a DNS SRV query ({@code _ldap._tcp.<dnsDomainName>}) to test whether this name is a
180  * hostname or a domain name served by one or more servers. If it turns out to be a domain name,
181  * you have to select one target host from the query response (according to RFC 2782), construct
182  * a domain-based SPN {@code ldap/<targetHost>/<dnsDomainName>@<REALM>} or a host-based
183  * one {@code ldap/<targetHost>@<REALM>}, obtain a service ticket for and connect to that target
184  * host.
185  * <p>
186  * <em>How to handle referrals?</em> There are several ways depending on your setup: Use the
187  * Global Catalog (port 3268) with a single forest and set referrals to {@code ignore}, or
188  * with multiple forests and set referrals to either
189  * <ul>
190  * <li>{@code follow} with a {@link DirContextSource} in your home forest and use my Active
191  * Directory DNS Locator, or</li>
192  * <li>{@code ignore} with multiple {@code DirContextSources}, and create a {@link CombinedRealm}
193  * with one {@code ActiveDirectoryRealm} per forest.</li>
194  * </ul>
195  *<p>
196  * You will then have the principal properly looked up in Active Directory.
197  * <p>
198  * Further references:
199  * <a href="https://technet.microsoft.com/en-us/library/cc759550%28v=ws.10%29.aspx">How DNS Support
200  * for Active Directory Works</a> is a good read on the DNS topic as well as
201  * <a href="https://technet.microsoft.com/en-us/library/cc978012.aspx">Global Catalog and LDAP
202  * Searches</a> and <a href="https://technet.microsoft.com/en-us/library/cc978014.aspx">LDAP
203  * Referrals</a>.
204  * <p>
205  * <strong>Note:</strong> Always remember, referrals incur an amplification in time and space and
206  * make the entire process slower.
207  * <br>
208  * <strong>Tip:</strong> Consider using the {@link PacDataActiveDirectoryRealm} if you don't need
209  * all features and use {@link SpnegoAuthenticator SPNEGO authentication} only since it is orders of
210  * magnitude faster, but remember though that {@link SSLAuthenticator X.509 authentication} still
211  * requires this realm.
212  *
213  * @see ActiveDirectoryPrincipal
214  */
215 public class ActiveDirectoryRealm extends ActiveDirectoryRealmBase {
216 
217 	// A mere holder class for directory server connections
218 	protected static class DirContextConnection {
219 		protected String id;
220 		protected long lastBorrowTime;
221 		protected DirContext context;
222 	}
223 
224 	private static final AtomicLong COUNT = new AtomicLong(0);
225 
226 	// 1.3.6.1.4.1.311.20.2.3
227 	private static final byte[] MS_UPN_OID_BYTES = { (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x04, (byte) 0x01,
228 			(byte) 0x82, (byte) 0x37, (byte) 0x14, (byte) 0x02, (byte) 0x03 };
229 
230 	private static final long UF_ACCOUNT_DISABLE = 0x00000002L;
231 	private static final long UF_NORMAL_ACCOUNT = 0x00000200L;
232 	private static final long UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000L;
233 
234 	private final static Oid MS_UPN;
235 	private final static Oid KRB5_NT_PRINCIPAL;
236 
237 	private final static Map<String, String> X500_PRINCIPAL_OID_MAP = new HashMap<String, String>();
238 
239 	private static final UsernameSearchMapper[] USERNAME_SEARCH_MAPPERS = {
240 			new SamAccountNameRfc2247Mapper(), new UserPrincipalNameSearchMapper() };
241 
242 	private static final String[] DEFAULT_USER_ATTRIBUTES = new String[] { "userAccountControl",
243 			"memberOf", "objectSid;binary", "sAMAccountName" };
244 
245 	private static final String[] DEFAULT_ROLE_ATTRIBUTES = new String[] { "groupType" };
246 
247 	private static final String DEFAULT_ROLE_FORMAT = "sid";
248 
249 	private static final Map<String, String[]> ROLE_FORMAT_ATTRIBUTES = new HashMap<>();
250 
251 	static {
252 		try {
253 			MS_UPN = new Oid("1.3.6.1.4.1.311.20.2.3");
254 		} catch (GSSException e) {
255 			throw new IllegalStateException("Failed to create OID for MS_UPN");
256 		}
257 
258 		try {
259 			KRB5_NT_PRINCIPAL = new Oid("1.2.840.113554.1.2.2.1");
260 		} catch (GSSException e) {
261 			throw new IllegalStateException("Failed to create OID for KRB5_NT_PRINCIPAL");
262 		}
263 
264 		X500_PRINCIPAL_OID_MAP.put("1.2.840.113549.1.9.1", "emailAddress");
265 		X500_PRINCIPAL_OID_MAP.put("2.5.4.5", "serialNumber");
266 		// surname
267 		X500_PRINCIPAL_OID_MAP.put("2.5.4.4", "SN");
268 		// givenName
269 		X500_PRINCIPAL_OID_MAP.put("2.5.4.42", "GN");
270 
271 		ROLE_FORMAT_ATTRIBUTES.put("sid", new String[] { "objectSid;binary", "sIDHistory;binary" });
272 		ROLE_FORMAT_ATTRIBUTES.put("name", new String [] { "msDS-PrincipalName" } );
273 		ROLE_FORMAT_ATTRIBUTES.put("nameEx", new String [] { "distinguishedName", "sAMAccountName" } );
274 	}
275 
276 	protected boolean localDirContextSource;
277 	protected String dirContextSourceName;
278 
279 	protected String[] attributes;
280 	protected String[] additionalAttributes;
281 
282 	protected String[] roleFormats;
283 	protected String[] roleAttributes;
284 
285 	protected boolean prependRoleFormat;
286 
287 	protected int connectionPoolSize = 0;
288 	protected long maxIdleTime = 900_000L;
289 
290 	// Poor man's connection pool
291 	protected SynchronizedStack<DirContextConnection> connectionPool;
292 
293 	protected static String getNextConnectionId() {
294 		return String.format("conn-%06d", COUNT.incrementAndGet());
295 	}
296 
297 	/**
298 	 * Sets whether the {@code DirContextSource} is locally ({@code context.xml} defined or globally
299 	 * {@code server.xml}.
300 	 *
301 	 * @param localDirContextSource
302 	 *            the local directory context source indication
303 	 */
304 	public void setLocalDirContextSource(boolean localDirContextSource) {
305 		this.localDirContextSource = localDirContextSource;
306 	}
307 
308 	/**
309 	 * Sets the name of the {@code DirContextSource}
310 	 *
311 	 * @param dirContextSourceName
312 	 *            the directory context source name
313 	 */
314 	public void setDirContextSourceName(String dirContextSourceName) {
315 		this.dirContextSourceName = dirContextSourceName;
316 	}
317 
318 	/**
319 	 * Sets a comma-separated list of Active Directory attributes retreived and stored for the user
320 	 * principal.
321 	 *
322 	 * @param additionalAttributes
323 	 *            the additional attributes
324 	 */
325 	public void setAdditionalAttributes(String additionalAttributes) {
326 		this.additionalAttributes = additionalAttributes.split(",");
327 
328 		this.attributes = new String[DEFAULT_USER_ATTRIBUTES.length + this.additionalAttributes.length];
329 			System.arraycopy(DEFAULT_USER_ATTRIBUTES, 0, this.attributes, 0,
330 					DEFAULT_USER_ATTRIBUTES.length);
331 			System.arraycopy(this.additionalAttributes, 0, this.attributes, DEFAULT_USER_ATTRIBUTES.length,
332 					this.additionalAttributes.length);
333 	}
334 
335 	/**
336 	 * Sets a comma-separated list of role formats to be applied to user security groups
337 	 * from Active Directory.
338 	 *
339 	 * @param roleFormats the role formats
340 	 */
341 	public void setRoleFormats(String roleFormats) {
342 		this.roleFormats = roleFormats.split(",");
343 		List<String> attributes = new ArrayList<>(Arrays.asList(DEFAULT_ROLE_ATTRIBUTES));
344 		for (String roleFormat : this.roleFormats) {
345 			if (ROLE_FORMAT_ATTRIBUTES.get(roleFormat) != null)
346 				attributes.addAll(Arrays.asList(ROLE_FORMAT_ATTRIBUTES.get(roleFormat)));
347 		}
348 
349 		this.roleAttributes = attributes.toArray(new String[0]);
350 	}
351 
352 	/**
353 	 * Sets whether the role format is prepended to the role.
354 	 *
355 	 * @param prependRoleFormat
356 	 *            the prepend role format indication
357 	 */
358 	public void setPrependRoleFormat(boolean prependRoleFormat) {
359 		this.prependRoleFormat = prependRoleFormat;
360 	}
361 
362 	/**
363 	 * Sets the maximum amount of directory server connections the pool will hold.
364 	 *
365 	 * @param connectionPoolSize
366 	 *            the connection pool size
367 	 */
368 	public void setConnectionPoolSize(int connectionPoolSize) {
369 		this.connectionPoolSize = connectionPoolSize;
370 	}
371 
372 	/**
373 	 * Sets the maximum amount of time in milliseconds a directory server connection should remain
374 	 * idle before it is closed.
375 	 *
376 	 * @param maxIdleTime
377 	 *            the maximum idle time
378 	 */
379 	public void setMaxIdleTime(long maxIdleTime) {
380 		this.maxIdleTime = maxIdleTime;
381 	}
382 
383 	@Override
384 	protected Principal getPrincipal(X509Certificate userCert) {
385 		try {
386 			Collection<List<?>> san = userCert.getSubjectAlternativeNames();
387 			if (san == null || san.isEmpty())
388 				return null;
389 
390 			String dn = userCert.getSubjectX500Principal().getName(X500Principal.RFC2253, X500_PRINCIPAL_OID_MAP);
391 			for (List<?> sanField : san) {
392 				Integer nameType = (Integer) sanField.get(0);
393 				// SAN's OtherName, see X509Certificate#getSubjectAlternativeNames() Javadoc
394 				if (nameType == 0) {
395 					byte[] otherName = (byte[]) sanField.get(1);
396 					if (logger.isDebugEnabled())
397 						logger.debug(sm.getString("activeDirectoryRealm.processingSanOtherName",
398 								Base64.getEncoder().encodeToString(otherName), dn));
399 					try {
400 						OtherNameParseResult result = OtherNameAsn1Parser.parse(otherName);
401 						if (Arrays.equals(result.getTypeId(), MS_UPN_OID_BYTES)) {
402 							Asn1Parser parser = new Asn1Parser(result.getValue());
403 							String upn = parser.parseUTF8String();
404 							if (logger.isDebugEnabled())
405 								logger.debug(sm.getString("activeDirectoryRealm.msUpnExtracted", upn, dn));
406 
407 							GSSName gssName = new StubGSSName(upn, MS_UPN);
408 
409 							return getPrincipal(gssName, null, true);
410 						}
411 					} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
412 						logger.warn(sm.getString("activeDirectoryRealm.sanOtherNameParsingFailed"), e);
413 					}
414 				}
415 			}
416 		} catch (CertificateParsingException e) {
417 			logger.warn(sm.getString("activeDirectoryRealm.sanParsingFailed"), e);
418 		}
419 
420 		return null;
421 	}
422 
423 	@Override
424 	protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
425 		if (gssName.isAnonymous())
426 			return new ActiveDirectoryPrincipal(gssName, Sid.ANONYMOUS_SID, gssCredential);
427 
428 		return getPrincipal(gssName, gssCredential, true);
429 	}
430 
431 	protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential, boolean retry) {
432 		DirContextConnection connection = null;
433 		try {
434 			connection = acquire();
435 
436 			try {
437 				User user = getUser(connection.context, gssName);
438 
439 				if (user != null) {
440 					List<String> roles = getRoles(connection.context, user);
441 
442 					return new ActiveDirectoryPrincipal(user.getGssName(), user.getSid(), roles, gssCredential,
443 							user.getAdditionalAttributes());
444 				}
445 			} catch (NamingException e) {
446 				// This construct is an ugly hack for
447 				// https://bugs.openjdk.java.net/browse/JDK-8273402
448 				boolean canRetry = false;
449 				if (e instanceof CommunicationException || e instanceof ServiceUnavailableException)
450 					canRetry = true;
451 				else {
452 					String explanation = e.getExplanation();
453 					if (explanation.equals("LDAP connection has been closed")
454 							|| explanation.startsWith("LDAP response read timed out, timeout used:"))
455 						canRetry = true;
456 				}
457 
458 				if (retry && canRetry) {
459 					logger.error(sm.getString("activeDirectoryRealm.principalSearchFailed.retry", gssName), e);
460 
461 					close(connection);
462 
463 					return getPrincipal(gssName, gssCredential, false);
464 				}
465 
466 				logger.error(sm.getString("activeDirectoryRealm.principalSearchFailed", gssName), e);
467 
468 				close(connection);
469 			}
470 		} catch (NamingException e) {
471 			logger.error(sm.getString("activeDirectoryRealm.acquire.namingException"), e);
472 		} finally {
473 			release(connection);
474 		}
475 
476 		return null;
477 	}
478 
479 	protected DirContextConnection acquire() throws NamingException {
480 		if (logger.isDebugEnabled())
481 			logger.debug(sm.getString("activeDirectoryRealm.acquire"));
482 
483 		DirContextConnection connection = null;
484 
485 		while (connection == null) {
486 			connection = connectionPool.pop();
487 
488 			if (connection != null) {
489 				long idleTime = System.currentTimeMillis() - connection.lastBorrowTime;
490 				// TODO support maxIdleTime = -1 (no expiry)
491 				if (idleTime > maxIdleTime) {
492 					if (logger.isDebugEnabled())
493 						logger.debug(sm.getString("activeDirectoryRealm.exceedMaxIdleTime", connection.id));
494 					close(connection);
495 					connection = null;
496 				} else {
497 					boolean valid = validate(connection);
498 					if (valid) {
499 						if (logger.isDebugEnabled())
500 							logger.debug(sm.getString("activeDirectoryRealm.reuse", connection.id));
501 					} else {
502 						close(connection);
503 						connection = null;
504 					}
505 				}
506 			} else {
507 				connection = new DirContextConnection();
508 				open(connection);
509 			}
510 		}
511 
512 		connection.lastBorrowTime = System.currentTimeMillis();
513 
514 		if (logger.isDebugEnabled())
515 			logger.debug(sm.getString("activeDirectoryRealm.acquired", connection.id));
516 
517 		return connection;
518 	}
519 
520 	protected boolean validate(DirContextConnection connection) {
521 		if (logger.isDebugEnabled())
522 			logger.debug(sm.getString("activeDirectoryRealm.validate", connection.id));
523 
524 		SearchControls controls = new SearchControls();
525 		controls.setSearchScope(SearchControls.OBJECT_SCOPE);
526 		controls.setCountLimit(1);
527 		controls.setReturningAttributes(new String[] { "objectClass" });
528 		// This applies to a server-side time limit which is actually translated to a second resolution and
529 		// does not apply to a socket read timeout
530 		controls.setTimeLimit(500);
531 
532 		NamingEnumeration<SearchResult> results = null;
533 		try {
534 			results = connection.context.search("", "objectclass=*", controls);
535 
536 			if (results.hasMore())
537 				return true;
538 		} catch (NamingException e) {
539 			logger.error(sm.getString("activeDirectoryRealm.validate.namingException", connection.id), e);
540 		} finally {
541 			close(results);
542 		}
543 
544 		return false;
545 	}
546 
547 	protected void release(DirContextConnection connection) {
548 		if (connection == null)
549 			return;
550 
551 		if (connection.context == null)
552 			return;
553 
554 		if (logger.isDebugEnabled())
555 			logger.debug(sm.getString("activeDirectoryRealm.release", connection.id));
556 		if (!connectionPool.push(connection))
557 			close(connection);
558 	}
559 
560 	protected void open(DirContextConnection connection) throws NamingException {
561 		javax.naming.Context context = null;
562 
563 		if (localDirContextSource) {
564 			context = ContextBindings.getClassLoader();
565 			context = (javax.naming.Context) context.lookup("comp/env");
566 		} else {
567 			Server server = getServer();
568 			context = server.getGlobalNamingContext();
569 		}
570 
571 		if (logger.isDebugEnabled())
572 			logger.debug(sm.getString("activeDirectoryRealm.open"));
573 		DirContextSource contextSource = (DirContextSource) context
574 				.lookup(dirContextSourceName);
575 		connection.context = contextSource.getDirContext();
576 		connection.id = getNextConnectionId();
577 		if (logger.isDebugEnabled())
578 			logger.debug(sm.getString("activeDirectoryRealm.opened", connection.id));
579 	}
580 
581 	protected void close(DirContextConnection connection) {
582 		if (connection.context == null)
583 			return;
584 
585 		try {
586 			if (logger.isDebugEnabled())
587 				logger.debug(sm.getString("activeDirectoryRealm.close", connection.id));
588 			connection.context.close();
589 			if (logger.isDebugEnabled())
590 				logger.debug(sm.getString("activeDirectoryRealm.closed", connection.id));
591 		} catch (NamingException e) {
592 			logger.error(sm.getString("activeDirectoryRealm.close.namingException", connection.id), e);
593 		}
594 
595 		connection.context = null;
596 	}
597 
598 	protected void close(NamingEnumeration<?> results) {
599 		if (results == null)
600 			return;
601 
602 		try {
603 			results.close();
604 		} catch (NamingException e) {
605 			; // Ignore
606 		}
607 	}
608 
609 	@Override
610 	protected void initInternal() throws LifecycleException {
611 		super.initInternal();
612 
613 		if (attributes == null)
614 			attributes = DEFAULT_USER_ATTRIBUTES;
615 
616 		if (roleFormats == null)
617 			setRoleFormats(DEFAULT_ROLE_FORMAT);
618 	}
619 
620 	@Override
621 	protected void startInternal() throws LifecycleException {
622 		connectionPool = new SynchronizedStack<>(connectionPoolSize, connectionPoolSize);
623 
624 		DirContextConnection connection = null;
625 		try {
626 			connection = acquire();
627 
628 			try {
629 				String referral = (String) connection.context.getEnvironment().get(DirContext.REFERRAL);
630 
631 				if ("follow".equals(referral))
632 					logger.warn(sm.getString("activeDirectoryRealm.referralFollow"));
633 			} catch (NamingException e) {
634 				logger.error(sm.getString("activeDirectoryRealm.environmentFailed"), e);
635 
636 				close(connection);
637 			}
638 		} catch (NamingException e) {
639 			logger.error(sm.getString("activeDirectoryRealm.acquire.namingException"), e);
640 		} finally {
641 			release(connection);
642 		}
643 
644 		super.startInternal();
645 	}
646 
647 	@Override
648 	protected void stopInternal() throws LifecycleException {
649 		super.stopInternal();
650 
651 		DirContextConnection connection = null;
652 		while ((connection = connectionPool.pop()) != null)
653 			close(connection);
654 
655 		connectionPool = null;
656 	}
657 
658 	private Oid getStringNameType(GSSName gssName) {
659 		try {
660 			return gssName.getStringNameType();
661 		} catch (GSSException e) {
662 			return null;
663 		}
664 	}
665 
666 	private String toRealm(Name distinguishedName) {
667 		LdapName dn = (LdapName) distinguishedName;
668 
669 		StringBuilder realm = new StringBuilder();
670 		for(Rdn rdn : dn.getRdns())
671 			if (rdn.getType().equalsIgnoreCase("DC"))
672 				realm.insert(0, ((String) rdn.getValue()).toUpperCase(Locale.ROOT) + ".");
673 
674 		if (realm.length() > 0)
675 			realm.deleteCharAt(realm.length() - 1);
676 
677 		return realm.toString();
678 	}
679 
680 	protected User getUser(DirContext context, GSSName gssName) throws NamingException {
681 		SearchControls searchCtls = new SearchControls();
682 		searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
683 		searchCtls.setReturningAttributes(attributes);
684 
685 		String searchFilter;
686 		Name searchBase = null;
687 		String searchAttributeName;
688 		String searchAttributeValue;
689 
690 		MappedValues mappedValues;
691 		NamingEnumeration<SearchResult> results = null;
692 		for (UsernameSearchMapper mapper : USERNAME_SEARCH_MAPPERS) {
693 			String mapperClassName = mapper.getClass().getSimpleName();
694 
695 			if (!mapper.supportsGssName(gssName)) {
696 				if (logger.isDebugEnabled())
697 					logger.debug(sm.getString("activeDirectoryRealm.nameTypeNotSupported", mapperClassName,
698 							getStringNameType(gssName), gssName));
699 
700 				continue;
701 			}
702 
703 			mappedValues = mapper.map(context, gssName);
704 
705 			searchBase = getRelativeName(context, mappedValues.getSearchBase());
706 			searchAttributeName = mappedValues.getSearchAttributeName();
707 			searchAttributeValue = mappedValues.getSearchUsername();
708 
709 			searchFilter = String.format("(%s={0})", searchAttributeName);
710 
711 			if (logger.isDebugEnabled())
712 				logger.debug(sm.getString("activeDirectoryRealm.usernameSearch",
713 						searchAttributeValue, searchBase, searchAttributeName, mapperClassName));
714 
715 			try {
716 				results = context.search(searchBase, searchFilter,
717 						new Object[] { searchAttributeValue }, searchCtls);
718 			} catch (ReferralException e) {
719 				logger.warn(sm.getString("activeDirectoryRealm.user.referralException",
720 						mapperClassName, e.getRemainingName(), e.getReferralInfo()));
721 
722 				continue;
723 			}
724 
725 			try {
726 				if (!results.hasMore()) {
727 					if (logger.isDebugEnabled())
728 						logger.debug(sm.getString("activeDirectoryRealm.userNotFoundWithMapper", gssName,
729 								mapperClassName));
730 
731 					close(results);
732 				} else
733 					break;
734 			} catch (PartialResultException e) {
735 				logger.debug(sm.getString("activeDirectoryRealm.user.partialResultException",
736 						mapperClassName, e.getRemainingName()));
737 
738 				close(results);
739 			}
740 		}
741 
742 		if (results == null || !results.hasMore()) {
743 			logger.debug(sm.getString("activeDirectoryRealm.userNotFound", gssName));
744 
745 			close(results);
746 			return null;
747 		}
748 
749 		SearchResult result = results.next();
750 
751 		try {
752 			if (results.hasMore()) {
753 				logger.error(sm.getString("activeDirectoryRealm.duplicateUser", gssName));
754 
755 				close(results);
756 				return null;
757 			}
758 		} catch (ReferralException e) {
759 			logger.warn(sm.getString("activeDirectoryRealm.duplicateUser.referralException", gssName,
760 					e.getRemainingName(), e.getReferralInfo()));
761 		} catch (PartialResultException e) {
762 			logger.debug(sm.getString("activeDirectoryRealm.duplicateUser.partialResultException", gssName,
763 					e.getRemainingName()));
764 		}
765 
766 		close(results);
767 
768 		Attributes userAttributes = result.getAttributes();
769 
770 		long userAccountControl = Long
771 				.parseLong((String) userAttributes.get("userAccountControl").get());
772 
773 		if ((userAccountControl & UF_ACCOUNT_DISABLE) != 0L) {
774 			logger.warn(sm.getString("activeDirectoryRealm.userFoundButDisabled", gssName));
775 			return null;
776 		}
777 
778 		if ((userAccountControl & UF_NORMAL_ACCOUNT) == 0L && (userAccountControl & UF_WORKSTATION_TRUST_ACCOUNT) == 0L) {
779 			logger.warn(sm.getString("activeDirectoryRealm.userFoundButNotSupported", gssName));
780 			return null;
781 		}
782 
783 		Name dn = getDistinguishedName(context, searchBase, result);
784 		byte[] sidBytes = (byte[]) userAttributes.get("objectSid;binary").get();
785 		Sid sid = new Sid(sidBytes);
786 
787 		if (logger.isDebugEnabled())
788 			logger.debug(sm.getString("activeDirectoryRealm.userFound", gssName, dn, sid));
789 
790 		if (!KRB5_NT_PRINCIPAL.equals(getStringNameType(gssName))) {
791 			String samAccountName = (String) userAttributes.get("sAMAccountName").get();
792 			String realm = toRealm(dn);
793 			String krb5Principal = samAccountName + "@" + realm;
794 
795 			if (logger.isTraceEnabled())
796 				logger.trace(sm.getString("activeDirectoryRealm.canonicalizingUser", getStringNameType(gssName),
797 						KRB5_NT_PRINCIPAL));
798 
799 			GSSName canonGssName = null;
800 			try {
801 				canonGssName = GSSManager.getInstance().createName(krb5Principal, KRB5_NT_PRINCIPAL);
802 			} catch (GSSException e) {
803 				logger.warn(sm.getString("activeDirectoryRealm.canonicalizeUserFailed", gssName));
804 				return null;
805 			}
806 
807 			if (logger.isDebugEnabled())
808 				logger.debug(sm.getString("activeDirectoryRealm.userCanonicalized", canonGssName));
809 
810 			gssName = canonGssName;
811 		}
812 
813 		Attribute memberOfAttr = userAttributes.get("memberOf");
814 
815 		List<String> memberOfs = new LinkedList<String>();
816 
817 		if (memberOfAttr != null && memberOfAttr.size() > 0) {
818 			NamingEnumeration<?> memberOfValues = memberOfAttr.getAll();
819 
820 			while (memberOfValues.hasMore())
821 				memberOfs.add((String) memberOfValues.next());
822 
823 			close(memberOfValues);
824 		}
825 
826 		Map<String, Object> additionalAttributesMap = Collections.emptyMap();
827 
828 		if (additionalAttributes != null && additionalAttributes.length > 0) {
829 			additionalAttributesMap = new HashMap<String, Object>();
830 
831 			for (String addAttr : additionalAttributes) {
832 				Attribute attr = userAttributes.get(addAttr);
833 
834 				if (attr != null && attr.size() > 0) {
835 					if (attr.size() > 1) {
836 						List<Object> attrList = new ArrayList<Object>(attr.size());
837 						NamingEnumeration<?> attrEnum = attr.getAll();
838 
839 						while (attrEnum.hasMore())
840 							attrList.add(attrEnum.next());
841 
842 						close(attrEnum);
843 
844 						additionalAttributesMap.put(addAttr,
845 								Collections.unmodifiableList(attrList));
846 					} else
847 						additionalAttributesMap.put(addAttr, attr.get());
848 				}
849 			}
850 		}
851 
852 		return new User(gssName, sid, memberOfs, additionalAttributesMap);
853 	}
854 
855 	protected List<String> getRoles(DirContext context, User user) throws NamingException {
856 		List<String> roles = new LinkedList<String>();
857 
858 		if (logger.isDebugEnabled())
859 			logger.debug(sm.getString("activeDirectoryRealm.retrievingRoles", user.getRoles().size(), user.getGssName()));
860 
861 		for (String role : user.getRoles()) {
862 			Name roleRdn = getRelativeName(context, role);
863 
864 			Attributes roleAttributes = null;
865 			try {
866 				roleAttributes = context.getAttributes(roleRdn, this.roleAttributes);
867 			} catch (ReferralException e) {
868 				logger.warn(sm.getString("activeDirectoryRealm.role.referralException", role,
869 						e.getRemainingName(), e.getReferralInfo()));
870 
871 				continue;
872 			} catch (PartialResultException e) {
873 				logger.debug(sm.getString("activeDirectoryRealm.role.partialResultException", role,
874 						e.getRemainingName()));
875 
876 				continue;
877 			}
878 
879 			int groupType = Integer.parseInt((String) roleAttributes.get("groupType").get());
880 
881 			// Skip distribution groups, i.e., we want security-enabled groups only
882 			// (ADS_GROUP_TYPE_SECURITY_ENABLED)
883 			if ((groupType & Integer.MIN_VALUE) == 0) {
884 				if (logger.isTraceEnabled())
885 					logger.trace(
886 							sm.getString("activeDirectoryRealm.skippingDistributionRole", role));
887 
888 				continue;
889 			}
890 
891 			for (String roleFormat: roleFormats) {
892 
893 				String roleFormatPrefix = prependRoleFormat ? roleFormat + ":" : "";
894 
895 				switch(roleFormat) {
896 				case "sid":
897 					byte[] objectSidBytes = (byte[]) roleAttributes.get("objectSid;binary").get();
898 					String sidString = new Sid(objectSidBytes).toString();
899 
900 					roles.add(roleFormatPrefix + sidString);
901 
902 					Attribute sidHistory = roleAttributes.get("sIDHistory;binary");
903 					List<String> sidHistoryStrings = new LinkedList<String>();
904 					if (sidHistory != null) {
905 						NamingEnumeration<?> sidHistoryEnum = sidHistory.getAll();
906 						while (sidHistoryEnum.hasMore()) {
907 							byte[] sidHistoryBytes = (byte[]) sidHistoryEnum.next();
908 							String sidHistoryString = new Sid(sidHistoryBytes).toString();
909 							sidHistoryStrings.add(sidHistoryString);
910 
911 							roles.add(roleFormatPrefix + sidHistoryString);
912 						}
913 
914 						close(sidHistoryEnum);
915 					}
916 
917 					if (logger.isTraceEnabled()) {
918 						if (sidHistoryStrings.isEmpty())
919 							logger.trace(sm.getString("activeDirectoryRealm.foundRoleSidConverted", role,
920 									sidString));
921 						else
922 							logger.trace(
923 									sm.getString("activeDirectoryRealm.foundRoleSidConverted.withSidHistory",
924 											role, sidString, sidHistoryStrings));
925 					}
926 					break;
927 				case "name":
928 					String msDsPrincipalName = (String) roleAttributes.get("msDS-PrincipalName").get();
929 
930 					roles.add(roleFormatPrefix + msDsPrincipalName);
931 
932 					if (logger.isTraceEnabled())
933 						logger.trace(sm.getString("activeDirectoryRealm.foundRoleNameConverted", role,
934 								msDsPrincipalName));
935 					break;
936 				case "nameEx":
937 						String distinguishedName = (String) roleAttributes.get("distinguishedName").get();
938 						String samAccountName = (String) roleAttributes.get("sAMAccountName").get();
939 
940 						NameParser parser = context.getNameParser(StringUtils.EMPTY);
941 						LdapName dn = (LdapName) parser.parse(distinguishedName);
942 						String realm = toRealm(dn);
943 						String nameEx = realm + "\\" + samAccountName;
944 
945 						roles.add(roleFormatPrefix + nameEx);
946 
947 						if (logger.isTraceEnabled())
948 							logger.trace(sm.getString("activeDirectoryRealm.foundRoleNameExConverted", role,
949 									nameEx));
950 					break;
951 				default:
952 					throw new IllegalArgumentException("The role format '" + roleFormat + "' is invalid");
953 				}
954 			}
955 		}
956 
957 		if (logger.isTraceEnabled())
958 			logger.trace(sm.getString("activeDirectoryRealm.foundRoles", roles.size(), user.getGssName(), roles));
959 		else if (logger.isDebugEnabled())
960 			logger.debug(sm.getString("activeDirectoryRealm.foundRolesCount", roles.size(),
961 					user.getGssName()));
962 
963 		return roles;
964 	}
965 
966 	/**
967 	 * Returns the distinguished name of a search result.
968 	 *
969 	 * @param context
970 	 *            Our DirContext
971 	 * @param baseName
972 	 *            The base DN
973 	 * @param result
974 	 *            The search result
975 	 * @return String containing the distinguished name
976 	 * @throws NamingException
977 	 *             if DN cannot be build
978 	 */
979 	protected Name getDistinguishedName(DirContext context, Name baseName, SearchResult result)
980 			throws NamingException {
981 		// Get the entry's distinguished name. For relative results, this means
982 		// we need to composite a name with the base name, the context name, and
983 		// the result name. For non-relative names, use the returned name.
984 		String resultName = result.getName();
985 		if (result.isRelative()) {
986 			NameParser parser = context.getNameParser(StringUtils.EMPTY);
987 			Name contextName = parser.parse(context.getNameInNamespace());
988 
989 			// Bugzilla 32269
990 			Name entryName = parser.parse(new CompositeName(resultName).get(0));
991 
992 			Name name = contextName.addAll(baseName);
993 			return name.addAll(entryName);
994 		} else {
995 			String absoluteName = result.getName();
996 			try {
997 				// Normalize the name by running it through the name parser.
998 				NameParser parser = context.getNameParser(StringUtils.EMPTY);
999 				URI userNameUri = new URI(resultName);
1000 				String pathComponent = userNameUri.getPath();
1001 				// Should not ever have an empty path component, since that is /{DN}
1002 				if (pathComponent.length() < 1) {
1003 					throw new InvalidNameException(
1004 							sm.getString("activeDirectoryRealm.unparseableName", absoluteName));
1005 				}
1006 				return parser.parse(pathComponent.substring(1));
1007 			} catch (URISyntaxException e) {
1008 				throw new InvalidNameException(
1009 						sm.getString("activeDirectoryRealm.unparseableName", absoluteName));
1010 			}
1011 		}
1012 	}
1013 
1014 	protected Name getRelativeName(DirContext context, String distinguishedName)
1015 			throws NamingException {
1016 		NameParser parser = context.getNameParser(StringUtils.EMPTY);
1017 		LdapName nameInNamespace = (LdapName) parser.parse(context.getNameInNamespace());
1018 		LdapName name = (LdapName) parser.parse(distinguishedName);
1019 
1020 		Rdn nameRdn;
1021 		Rdn nameInNamespaceRdn;
1022 
1023 		while (Math.min(name.size(), nameInNamespace.size()) != 0) {
1024 			nameRdn = name.getRdn(0);
1025 			nameInNamespaceRdn = nameInNamespace.getRdn(0);
1026 			if (nameRdn.equals(nameInNamespaceRdn)) {
1027 				name.remove(0);
1028 				nameInNamespace.remove(0);
1029 			} else
1030 				break;
1031 		}
1032 
1033 		int innerPosn;
1034 		while (Math.min(name.size(), nameInNamespace.size()) != 0) {
1035 			innerPosn = nameInNamespace.size() - 1;
1036 			nameRdn = name.getRdn(0);
1037 			nameInNamespaceRdn = nameInNamespace.getRdn(innerPosn);
1038 			if (nameRdn.equals(nameInNamespaceRdn)) {
1039 				name.remove(0);
1040 				nameInNamespace.remove(innerPosn);
1041 			} else
1042 				break;
1043 		}
1044 
1045 		return name;
1046 	}
1047 
1048 	protected static class User {
1049 
1050 		private final GSSName gssName;
1051 		private final Sid sid;
1052 		private final List<String> roles;
1053 		private final Map<String, Object> additionalAttributes;
1054 
1055 		public User(GSSName gssName, Sid sid, List<String> roles,
1056 				Map<String, Object> additionalAttributes) {
1057 			this.gssName = gssName;
1058 			this.sid = sid;
1059 			this.roles = roles;
1060 			this.additionalAttributes = additionalAttributes;
1061 		}
1062 
1063 		public GSSName getGssName() {
1064 			return gssName;
1065 		}
1066 
1067 		public Sid getSid() {
1068 			return sid;
1069 		}
1070 
1071 		public List<String> getRoles() {
1072 			return roles;
1073 		}
1074 
1075 		public Map<String, Object> getAdditionalAttributes() {
1076 			return additionalAttributes;
1077 		}
1078 
1079 	}
1080 
1081 }