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