View Javadoc
1   /*
2    * Copyright 2013–2020 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.util.ArrayList;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.naming.CompositeName;
29  import javax.naming.InvalidNameException;
30  import javax.naming.Name;
31  import javax.naming.NameParser;
32  import javax.naming.NamingEnumeration;
33  import javax.naming.NamingException;
34  import javax.naming.PartialResultException;
35  import javax.naming.ReferralException;
36  import javax.naming.directory.Attribute;
37  import javax.naming.directory.Attributes;
38  import javax.naming.directory.DirContext;
39  import javax.naming.directory.InitialDirContext;
40  import javax.naming.directory.SearchControls;
41  import javax.naming.directory.SearchResult;
42  import javax.naming.ldap.LdapName;
43  import javax.naming.ldap.ManageReferralControl;
44  import javax.naming.ldap.Rdn;
45  import javax.security.sasl.SaslClient;
46  
47  import net.sf.michaelo.dirctxsrc.DirContextSource;
48  import net.sf.michaelo.tomcat.realm.mapper.SamAccountNameRfc2247Mapper;
49  import net.sf.michaelo.tomcat.realm.mapper.UserPrincipalNameSearchMapper;
50  import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper;
51  import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper.MappedValues;
52  
53  import org.apache.catalina.LifecycleException;
54  import org.apache.catalina.Server;
55  import org.apache.catalina.realm.CombinedRealm;
56  import org.apache.commons.lang3.StringUtils;
57  import org.apache.naming.ContextBindings;
58  import org.apache.tomcat.util.collections.SynchronizedStack;
59  import org.ietf.jgss.GSSCredential;
60  import org.ietf.jgss.GSSName;
61  
62  /**
63   * A realm which retrieves authenticated users from Active Directory.
64   *
65   * <h2>Configuration</h2> Following options can be configured:
66   * <ul>
67   * <li>{@code dirContextSourceName}: the name of the {@link DirContextSource} in JNDI with which
68   * principals will be retrieved.</li>
69   * <li>{@code localDirContextSource}: whether this {@code DirContextSource} is locally configured in
70   * the {@code context.xml} or globally configured in the {@code server.xml} (optional). Default
71   * value is {@code false}.</li>
72   * <li>{@code additionalAttributes}: comma-separated list of attributes to be retrieved for the
73   * principal. Binary attributes must end with {@code ;binary} and will be stored as {@code byte[]},
74   * ordinary attributes will be stored as {@code String}. If an attribute is multivalued, it will be
75   * stored as {@code List}.</li>
76   * <li>{@code connectionPoolSize}: the maximum amount of directory server connections the pool will
77   * hold. Default is zero which means no connections will be pooled.
78   * <li>{@code maxIdleTime}: the maximum amount of time in milliseconds a directory server connection
79   * should remain idle before it is closed. Default value is 15 minutes.</li>
80   * </ul>
81   * <br>
82   * <strong>Note</strong>: By default the SIDs ({@code objectSid} and {@code sIDHistory}) of the
83   * Active Directory security groups will be retrieved and no further configuration is required for
84   * them.
85   *
86   * <h2>Connection Pooling</h2> This realm offers a poor man's directory server connection pooling
87   * which can drastically improve access performance for non-session (stateless) applications. It
88   * utilizes a LIFO structure based on {@link SynchronizedStack}. No background thread is managing
89   * the connections. They are acquired, validated, eventually closed and opened when
90   * {@link #getPrincipal(GSSName, GSSCredential)} is invoked. Validation involves a minimal and
91   * limited query with at most 500 ms of wait time just to verify the connection is alive and
92   * healthy. If the query fails, the connection is closed immediately. If the amount of requested
93   * connections exceeds ones the available in the pool, new ones are opened and pushed onto the pool.
94   * If the pool does not accept any addtional connetions they are closed immediately. <br>
95   * This connection pool feature has to be explicitly enabled by setting {@code connectionPoolSize}
96   * to greater than zero.
97   *
98   * <h2 id="referral-handling">Referral Handling</h2> When working with the default LDAP ports (not
99   * GC) or in a multi-forest environment, it is highly likely to receive referrals (either
100  * subordinate or cross) during a search or lookup. JNDI takes several approaches to handle
101  * referrals with the {@code java.naming.referral} property and its values: {@code ignore},
102  * {@code throw}, and {@code follow}. You can ignore referrals altogether, but the Active Directory
103  * will still signal a {@link PartialResultException} when a {@link NamingEnumeration} is iterated.
104  * The reason is because Oracle's LDAP implementation adds a {@link ManageReferralControl} when
105  * {@code ignore} is set but Active Directory does not support it and returns a referral anyway.
106  * This realm will catch this and continue to process the enumeration. If the
107  * {@code DirContextSource} is set to {@code throw}, this realm will catch the
108  * {@link ReferralException} but avoid to follow the referral(s) manually (for several reasons) and
109  * will continue with the process. Following referrals automatically is a completely opaque
110  * operation to the application, the {@code ReferralException} is handled internally and referral
111  * contexts are queried and closed. Unfortunately, Oracle's LDAP implementation is not able to
112  * handle this properly and only Oracle can fix this shortcoming. Issues have already been reported
113  * (Review IDs 9089870 and 9089874, public issues
114  * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8161361">JDK-8161361</a> and
115  * <a href="https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8160768">JDK-8161361</a>)!
116  * <p>
117  * <em>What is the shortcoming and how can it be solved?</em> Microsoft takes a very sophisticated
118  * approach on not to rely on host names because servers can be provisioned and decommissioned any
119  * time. Instead, they heavily rely on DNS domain names and DNS SRV records at runtime. I.e., an
120  * initial or a referral URL does not contain a host name, but only a DNS domain name. While you can
121  * connect to the service with this name, you cannot easily authenticate against it with Kerberos
122  * because one cannot bind the same SPN {@code ldap/<dnsDomainName>@<REALM>}, e.g.,
123  * {@code ldap/example.com@EXAMPLE.COM} to more than one account. If you try authenticate anyway,
124  * you will receive a "Server not found in Kerberos database (7)" error. Therefore, one has to
125  * perform a DNS SRV query ({@code _ldap._tcp.<dnsDomainName>}) to test whether this name is a host
126  * name or a DNS domain name served by one or more machines. If it turns out to be a DNS domain
127  * name, you have to select one target host from the query response (according to RFC 2782),
128  * construct a domain-based SPN {@code ldap/<targetHost>/<dnsDomainName>@<REALM>} or a host-based
129  * one {@code ldap/<targetHost>@<REALM>}, obtain a service ticket for and connect to that target
130  * host. If it is a regular host name, which is not the usual case with Active Directory, Oracle's
131  * internal implementation will behave correctly.<br>
132  * The {@code follow} implementation cannot be made to work because there is no way to tell the
133  * internal classes to perform this DNS SRV query and pass the appropriate server name(s) for the
134  * SPN to the {@link SaslClient}. It is deemed to fail. Note, that host name canocalization might
135  * sound reasonable within the {@code SaslClient}, but this is deemed to fail too for two reasons:
136  * First, the {@code SaslClient} will receive an arbitrary IP address without knowing whether the
137  * LDAP client socket will use the same one. You will have a service ticket issued for another host
138  * and your authentication will fail. Second, most Kerberos implementations rely on reverse DNS
139  * records, but Microsoft's Active Directory concept does not care about reverse DNS, it does not
140  * canonicalize host names by default and there is no guarantee, that reverse DNS is set up
141  * properly. Some environments do not even have control over the reverse DNS zone ({@code PTR}
142  * records). Using {@code throw} will not make it any better because the referral URL returned by
143  * {@link ReferralException#getReferralInfo()} cannot be changed with the calculated value(s) from
144  * DNS. {@link ReferralException#getReferralContext()} will unconditionally reuse that value. The
145  * only way (theoretically) to achieve this is to construct an {@link InitialDirContext} with the
146  * new URL manually and work with it appropriately. Though, this approach has not been evaluated and
147  * at this time and won't be implemented. (Changing the URLs manually in the debugger makes it work
148  * actually)
149  * <p>
150  * <em>How to work around this issue?</em> There are several ways depending on your setup: Use the
151  * Global Catalog (port 3268) with
152  * <ul>
153  * <li>a single forest and set referrals to {@code ignore}, or</li>
154  * <li>multiple forests and set referrals to either
155  * <ul>
156  * <li>{@code follow} or {@code throw} with a {@link DirContextSource} in your home forest, patch
157  * {@code com.sun.jndi.ldap.LdapCtxFactory} to properly resolve DNS domain names to host names and
158  * prepend it to the boot classpath and all referrals will be cleanly resolved, or</li>
159  * <li>{@code ignore} with multiple {@code DirContextSources}, and create a {@link CombinedRealm}
160  * with one {@code ActiveDirectoryRealm} per forest.</li>
161  * </ul>
162  * </li>
163  * </ul>
164  *
165  * You will then have the principal properly looked up in the Active Directory.
166  * <p>
167  * This issue is also documented on <a href="https://stackoverflow.com/q/25436410/696632">Stack
168  * Overflow</a>. Additionally,
169  * <a href="https://technet.microsoft.com/en-us/library/cc759550%28v=ws.10%29.aspx">How DNS Support
170  * for Active Directory Works</a> is a good read on the DNS topic as well as
171  * <a href="https://technet.microsoft.com/en-us/library/cc978012.aspx">Global Catalog and LDAP
172  * Searches</a> and <a href="https://technet.microsoft.com/en-us/library/cc978014.aspx">LDAP
173  * Referrals</a>.
174  * <p>
175  * <strong>Note:</strong> always remember, referrals incur an amplification in time and space and
176  * make the entire process slower.
177  *
178  * @see ActiveDirectoryPrincipal
179  * @version $Id: ActiveDirectoryRealm.java 360 2020-11-25 17:10:34Z michael-o $
180  */
181 public class ActiveDirectoryRealm extends ActiveDirectoryRealmBase {
182 
183 	// A mere holder class for directory server connections
184 	protected static class DirContextConnection {
185 		protected long lastBorrowTime;
186 		protected DirContext context;
187 	}
188 
189 	private static final UsernameSearchMapper[] USERNAME_SEARCH_MAPPERS = {
190 			new SamAccountNameRfc2247Mapper(), new UserPrincipalNameSearchMapper() };
191 
192 	private static final String[] DEFAULT_USER_ATTRIBUTES = new String[] { "userAccountControl",
193 			"memberOf", "objectSid;binary" };
194 
195 	private static final String[] DEFAULT_ROLE_ATTRIBUTES = new String[] { "groupType",
196 			"objectSid;binary", "sIDHistory;binary" };
197 
198 	protected boolean localDirContextSource;
199 	protected String dirContextSourceName;
200 	protected String[] additionalAttributes;
201 	protected int connectionPoolSize = 0;
202 	protected long maxIdleTime = 900_000L;
203 
204 	// Poor man's connection pool
205 	protected SynchronizedStack<DirContextConnection> connectionPool;
206 
207 	/**
208 	 * Descriptive information about this Realm implementation.
209 	 */
210 	protected static final String name = "ActiveDirectoryRealm";
211 
212 	/**
213 	 * Sets whether the {@code DirContextSource} is locally ({@code context.xml} defined or globally
214 	 * {@code server.xml}.
215 	 *
216 	 * @param localDirContextSource
217 	 *            the local directory context source indication
218 	 */
219 	public void setLocalDirContextSource(boolean localDirContextSource) {
220 		this.localDirContextSource = localDirContextSource;
221 	}
222 
223 	/**
224 	 * Sets the name of the {@code DirContextSource}
225 	 *
226 	 * @param dirContextSourceName
227 	 *            the directory context source name
228 	 */
229 	public void setDirContextSourceName(String dirContextSourceName) {
230 		this.dirContextSourceName = dirContextSourceName;
231 	}
232 
233 	/**
234 	 * Sets a comma-separated list of Active Directory attributes retreived and stored for the user
235 	 * principal.
236 	 *
237 	 * @param additionalAttributes
238 	 *            the additional attributes
239 	 */
240 	public void setAdditionalAttributes(String additionalAttributes) {
241 		this.additionalAttributes = additionalAttributes.split(",");
242 	}
243 
244 	/**
245 	 * Sets the maximum amount of directory server connections the pool will hold.
246 	 *
247 	 * @param connectionPoolSize
248 	 *            the connection pool size
249 	 */
250 	public void setConnectionPoolSize(int connectionPoolSize) {
251 		this.connectionPoolSize = connectionPoolSize;
252 	}
253 
254 	/**
255 	 * Sets the maximum amount of time in milliseconds a directory server connection should remain
256 	 * idle before it is closed.
257 	 *
258 	 * @param maxIdleTime
259 	 *            the maximum idle time
260 	 */
261 	public void setMaxIdleTime(long maxIdleTime) {
262 		this.maxIdleTime = maxIdleTime;
263 	}
264 
265 	@Override
266 	protected String getName() {
267 		return name;
268 	}
269 
270 	@Override
271 	protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
272 		if (gssName.isAnonymous())
273 			return new ActiveDirectoryPrincipal(gssName, Sid.ANONYMOUS_SID, gssCredential);
274 
275 		DirContextConnection connection = acquire();
276 		if (connection.context == null)
277 			return null;
278 
279 		try {
280 			User user = getUser(connection.context, gssName);
281 
282 			if (user != null) {
283 				List<String> roles = getRoles(connection.context, user);
284 
285 				return new ActiveDirectoryPrincipal(gssName, user.getSid(), roles, gssCredential,
286 						user.getAdditionalAttributes());
287 			}
288 		} catch (NamingException e) {
289 			logger.error(sm.getString("activeDirectoryRealm.principalSearchFailed", gssName), e);
290 
291 			close(connection);
292 		} finally {
293 			release(connection);
294 		}
295 
296 		return null;
297 	}
298 
299 	protected DirContextConnection acquire() {
300 		if (logger.isDebugEnabled())
301 			logger.debug(sm.getString("activeDirectoryRealm.acquire"));
302 
303 		DirContextConnection connection = null;
304 
305 		while (connection == null) {
306 			connection = connectionPool.pop();
307 
308 			if (connection != null) {
309 				long idleTime = System.currentTimeMillis() - connection.lastBorrowTime;
310 				// TODO support maxIdleTime = -1 (no expiry)
311 				if (idleTime > maxIdleTime) {
312 					if (logger.isDebugEnabled())
313 						logger.debug(sm.getString("activeDirectoryRealm.exceedMaxIdleTime"));
314 					close(connection);
315 					connection = null;
316 				} else {
317 					boolean valid = validate(connection);
318 					if (valid) {
319 						if (logger.isDebugEnabled())
320 							logger.debug(sm.getString("activeDirectoryRealm.reuse"));
321 					} else {
322 						close(connection);
323 						connection = null;
324 					}
325 				}
326 			} else {
327 				connection = new DirContextConnection();
328 				open(connection);
329 			}
330 		}
331 
332 		connection.lastBorrowTime = System.currentTimeMillis();
333 
334 		return connection;
335 	}
336 
337 	protected boolean validate(DirContextConnection connection) {
338 		if (logger.isDebugEnabled())
339 			logger.debug(sm.getString("activeDirectoryRealm.validate"));
340 
341 		SearchControls controls = new SearchControls();
342 		controls.setSearchScope(SearchControls.OBJECT_SCOPE);
343 		controls.setCountLimit(1);
344 		controls.setReturningAttributes(new String[] { "objectClass" });
345 		controls.setTimeLimit(500);
346 
347 		NamingEnumeration<SearchResult> results = null;
348 		try {
349 			results = connection.context.search("", "objectclass=*", controls);
350 
351 			if (results.hasMore()) {
352 				close(results);
353 				return true;
354 			}
355 		} catch (NamingException e) {
356 			logger.error(sm.getString("activeDirectoryRealm.validate.namingException"), e);
357 
358 			return false;
359 		}
360 
361 		close(results);
362 
363 		return false;
364 	}
365 
366 	protected void release(DirContextConnection connection) {
367 		if (connection.context == null)
368 			return;
369 
370 		if (logger.isDebugEnabled())
371 			logger.debug(sm.getString("activeDirectoryRealm.release"));
372 		if (!connectionPool.push(connection))
373 			close(connection);
374 	}
375 
376 	protected void open(DirContextConnection connection) {
377 		try {
378 			javax.naming.Context context = null;
379 
380 			if (localDirContextSource) {
381 				context = ContextBindings.getClassLoader();
382 				context = (javax.naming.Context) context.lookup("comp/env");
383 			} else {
384 				Server server = getServer();
385 				context = server.getGlobalNamingContext();
386 			}
387 
388 			if (logger.isDebugEnabled())
389 				logger.debug(sm.getString("activeDirectoryRealm.open"));
390 			DirContextSource contextSource = (DirContextSource) context
391 					.lookup(dirContextSourceName);
392 			connection.context = contextSource.getDirContext();
393 		} catch (NamingException e) {
394 			logger.error(sm.getString("activeDirectoryRealm.open.namingException"), e);
395 		}
396 	}
397 
398 	protected void close(DirContextConnection connection) {
399 		if (connection.context == null)
400 			return;
401 
402 		try {
403 			if (logger.isDebugEnabled())
404 				logger.debug(sm.getString("activeDirectoryRealm.close"));
405 			connection.context.close();
406 		} catch (NamingException e) {
407 			logger.error(sm.getString("activeDirectoryRealm.close.namingException"), e);
408 		}
409 
410 		connection.context = null;
411 	}
412 
413 	protected void close(NamingEnumeration<?> results) {
414 		if (results == null)
415 			return;
416 
417 		try {
418 			results.close();
419 		} catch (NamingException e) {
420 			; // swallow
421 		}
422 	}
423 
424 	@Override
425 	protected void startInternal() throws LifecycleException {
426 		connectionPool = new SynchronizedStack<>(connectionPoolSize, connectionPoolSize);
427 
428 		DirContextConnection connection = acquire();
429 		if (connection.context == null)
430 			return;
431 
432 		try {
433 			String referral = (String) connection.context.getEnvironment().get(DirContext.REFERRAL);
434 
435 			if ("follow".equals(referral))
436 				logger.warn(sm.getString("activeDirectoryRealm.referralFollow"));
437 		} catch (NamingException e) {
438 			logger.error(sm.getString("activeDirectoryRealm.environmentFailed"), e);
439 
440 			close(connection);
441 		} finally {
442 			release(connection);
443 		}
444 
445 		super.startInternal();
446 	}
447 
448 	@Override
449 	protected void stopInternal() throws LifecycleException {
450 		super.stopInternal();
451 
452 		DirContextConnection connection = null;
453 		while ((connection = connectionPool.pop()) != null)
454 			close(connection);
455 
456 		connectionPool = null;
457 	}
458 
459 	protected User getUser(DirContext context, GSSName gssName) throws NamingException {
460 		String[] attributes = DEFAULT_USER_ATTRIBUTES;
461 
462 		if (additionalAttributes != null && additionalAttributes.length > 0) {
463 			attributes = new String[DEFAULT_USER_ATTRIBUTES.length + additionalAttributes.length];
464 			System.arraycopy(DEFAULT_USER_ATTRIBUTES, 0, attributes, 0,
465 					DEFAULT_USER_ATTRIBUTES.length);
466 			System.arraycopy(additionalAttributes, 0, attributes, DEFAULT_USER_ATTRIBUTES.length,
467 					additionalAttributes.length);
468 		}
469 
470 		SearchControls searchCtls = new SearchControls();
471 		searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
472 		searchCtls.setReturningAttributes(attributes);
473 
474 		// Query for user and machine accounts only
475 		String searchFilterPattern = "(&(|(sAMAccountType=805306368)(sAMAccountType=805306369))(%s={0}))";
476 
477 		String searchFilter;
478 		Name searchBase = null;
479 		String searchAttributeName;
480 		String searchAttributeValue;
481 
482 		MappedValues mappedValues;
483 		NamingEnumeration<SearchResult> results = null;
484 		for (UsernameSearchMapper mapper : USERNAME_SEARCH_MAPPERS) {
485 			String mapperClassName = mapper.getClass().getSimpleName();
486 			mappedValues = mapper.map(context, gssName);
487 
488 			searchBase = getRelativeName(context, mappedValues.getSearchBase());
489 			searchAttributeName = mappedValues.getSearchAttributeName();
490 			searchAttributeValue = mappedValues.getSearchUsername();
491 
492 			searchFilter = String.format(searchFilterPattern, searchAttributeName);
493 
494 			if (logger.isDebugEnabled())
495 				logger.debug(sm.getString("activeDirectoryRealm.usernameSearch",
496 						searchAttributeValue, searchBase, searchAttributeName, mapperClassName));
497 
498 			try {
499 				results = context.search(searchBase, searchFilter,
500 						new Object[] { searchAttributeValue }, searchCtls);
501 			} catch (ReferralException e) {
502 				logger.warn(sm.getString("activeDirectoryRealm.user.referralException",
503 						mapperClassName, e.getRemainingName(), e.getReferralInfo()));
504 
505 				continue;
506 			}
507 
508 			try {
509 				if (!results.hasMore()) {
510 					if (logger.isDebugEnabled())
511 						logger.debug(sm.getString("activeDirectoryRealm.userNotMapped", gssName,
512 								mapperClassName));
513 
514 					close(results);
515 				} else
516 					break;
517 			} catch (PartialResultException e) {
518 				logger.debug(sm.getString("activeDirectoryRealm.user.partialResultException",
519 						mapperClassName, e.getRemainingName()));
520 
521 				close(results);
522 			}
523 		}
524 
525 		if (results == null) {
526 			logger.debug(sm.getString("activeDirectoryRealm.userNotFound", gssName));
527 
528 			return null;
529 		}
530 
531 		SearchResult result = results.next();
532 
533 		if (results.hasMore()) {
534 			logger.error(sm.getString("activeDirectoryRealm.duplicateUser", gssName));
535 
536 			close(results);
537 			return null;
538 		}
539 
540 		Attributes userAttributes = result.getAttributes();
541 
542 		int userAccountControl = Integer
543 				.parseInt((String) userAttributes.get("userAccountControl").get());
544 
545 		// Do not allow disabled accounts (UF_ACCOUNT_DISABLE)
546 		if ((userAccountControl & 0x02) != 0) {
547 			logger.warn(sm.getString("activeDirectoryRealm.userFoundButDisabled", gssName));
548 
549 			close(results);
550 			return null;
551 		}
552 
553 		Name dn = getDistinguishedName(context, searchBase, result);
554 		byte[] sidBytes = (byte[]) userAttributes.get("objectSid;binary").get();
555 		Sid sid = new Sid(sidBytes);
556 
557 		if (logger.isDebugEnabled())
558 			logger.debug(sm.getString("activeDirectoryRealm.userFound", gssName, dn, sid));
559 
560 		Attribute memberOfAttr = userAttributes.get("memberOf");
561 
562 		List<String> roles = new LinkedList<String>();
563 
564 		if (memberOfAttr != null && memberOfAttr.size() > 0) {
565 			NamingEnumeration<?> memberOfValues = memberOfAttr.getAll();
566 
567 			while (memberOfValues.hasMore())
568 				roles.add((String) memberOfValues.next());
569 
570 			close(memberOfValues);
571 		}
572 
573 		Map<String, Object> additionalAttributesMap = Collections.emptyMap();
574 
575 		if (additionalAttributes != null && additionalAttributes.length > 0) {
576 			additionalAttributesMap = new HashMap<String, Object>();
577 
578 			for (String addAttr : additionalAttributes) {
579 				Attribute attr = userAttributes.get(addAttr);
580 
581 				if (attr != null && attr.size() > 0) {
582 					if (attr.size() > 1) {
583 						List<Object> attrList = new ArrayList<Object>(attr.size());
584 						NamingEnumeration<?> attrEnum = attr.getAll();
585 
586 						while (attrEnum.hasMore())
587 							attrList.add(attrEnum.next());
588 
589 						close(attrEnum);
590 
591 						additionalAttributesMap.put(addAttr,
592 								Collections.unmodifiableList(attrList));
593 					} else
594 						additionalAttributesMap.put(addAttr, attr.get());
595 				}
596 			}
597 		}
598 
599 		close(results);
600 		return new User(gssName, sid, roles, additionalAttributesMap);
601 	}
602 
603 	protected List<String> getRoles(DirContext context, User user) throws NamingException {
604 		List<String> roles = new LinkedList<String>();
605 
606 		if (logger.isDebugEnabled())
607 			logger.debug(sm.getString("activeDirectoryRealm.retrievingRoles", user.getGssName()));
608 
609 		for (String role : user.getRoles()) {
610 			Name roleRdn = getRelativeName(context, role);
611 
612 			Attributes roleAttributes = null;
613 			try {
614 				roleAttributes = context.getAttributes(roleRdn, DEFAULT_ROLE_ATTRIBUTES);
615 			} catch (ReferralException e) {
616 				logger.warn(sm.getString("activeDirectoryRealm.role.referralException", role,
617 						e.getRemainingName(), e.getReferralInfo()));
618 
619 				continue;
620 			} catch (PartialResultException e) {
621 				logger.debug(sm.getString("activeDirectoryRealm.role.partialResultException", role,
622 						e.getRemainingName()));
623 
624 				continue;
625 			}
626 
627 			int groupType = Integer.parseInt((String) roleAttributes.get("groupType").get());
628 
629 			// Skip distribution groups, i.e., we want security-enabled groups only
630 			// (ADS_GROUP_TYPE_SECURITY_ENABLED)
631 			if ((groupType & Integer.MIN_VALUE) == 0) {
632 				if (logger.isTraceEnabled())
633 					logger.trace(
634 							sm.getString("activeDirectoryRealm.skippingDistributionRole", role));
635 
636 				continue;
637 			}
638 
639 			byte[] objectSidBytes = (byte[]) roleAttributes.get("objectSid;binary").get();
640 			String sidString = new Sid(objectSidBytes).toString();
641 
642 			Attribute sidHistory = roleAttributes.get("sIDHistory;binary");
643 			List<String> sidHistoryStrings = new LinkedList<String>();
644 			if (sidHistory != null) {
645 				NamingEnumeration<?> sidHistoryEnum = sidHistory.getAll();
646 				while (sidHistoryEnum.hasMore()) {
647 					byte[] sidHistoryBytes = (byte[]) sidHistoryEnum.next();
648 					sidHistoryStrings.add(new Sid(sidHistoryBytes).toString());
649 				}
650 
651 				close(sidHistoryEnum);
652 			}
653 
654 			roles.add(sidString);
655 			roles.addAll(sidHistoryStrings);
656 
657 			if (logger.isTraceEnabled()) {
658 				if (sidHistoryStrings.isEmpty())
659 					logger.trace(sm.getString("activeDirectoryRealm.foundRoleConverted", role,
660 							sidString));
661 				else
662 					logger.trace(
663 							sm.getString("activeDirectoryRealm.foundRoleConverted.withSidHistory",
664 									role, sidString, sidHistoryStrings));
665 			}
666 		}
667 
668 		if (logger.isDebugEnabled())
669 			logger.debug(sm.getString("activeDirectoryRealm.foundRolesCount", roles.size(),
670 					user.getGssName()));
671 		if (logger.isTraceEnabled())
672 			logger.trace(sm.getString("activeDirectoryRealm.foundRoles", user.getGssName(), roles));
673 
674 		return roles;
675 	}
676 
677 	/**
678 	 * Returns the distinguished name of a search result.
679 	 *
680 	 * @param context
681 	 *            Our DirContext
682 	 * @param baseName
683 	 *            The base DN
684 	 * @param result
685 	 *            The search result
686 	 * @return String containing the distinguished name
687 	 * @throws NamingException
688 	 *             if DN cannot be build
689 	 */
690 	protected Name getDistinguishedName(DirContext context, Name baseName, SearchResult result)
691 			throws NamingException {
692 		// Get the entry's distinguished name. For relative results, this means
693 		// we need to composite a name with the base name, the context name, and
694 		// the result name. For non-relative names, use the returned name.
695 		String resultName = result.getName();
696 		if (result.isRelative()) {
697 			NameParser parser = context.getNameParser(StringUtils.EMPTY);
698 			Name contextName = parser.parse(context.getNameInNamespace());
699 
700 			// Bugzilla 32269
701 			Name entryName = parser.parse(new CompositeName(resultName).get(0));
702 
703 			Name name = contextName.addAll(baseName);
704 			return name.addAll(entryName);
705 		} else {
706 			String absoluteName = result.getName();
707 			try {
708 				// Normalize the name by running it through the name parser.
709 				NameParser parser = context.getNameParser(StringUtils.EMPTY);
710 				URI userNameUri = new URI(resultName);
711 				String pathComponent = userNameUri.getPath();
712 				// Should not ever have an empty path component, since that is /{DN}
713 				if (pathComponent.length() < 1) {
714 					throw new InvalidNameException(
715 							sm.getString("activeDirectoryRealm.unparseableName", absoluteName));
716 				}
717 				return parser.parse(pathComponent.substring(1));
718 			} catch (URISyntaxException e) {
719 				throw new InvalidNameException(
720 						sm.getString("activeDirectoryRealm.unparseableName", absoluteName));
721 			}
722 		}
723 	}
724 
725 	protected Name getRelativeName(DirContext context, String distinguishedName)
726 			throws NamingException {
727 		NameParser parser = context.getNameParser(StringUtils.EMPTY);
728 		LdapName nameInNamespace = (LdapName) parser.parse(context.getNameInNamespace());
729 		LdapName name = (LdapName) parser.parse(distinguishedName);
730 
731 		Rdn nameRdn;
732 		Rdn nameInNamespaceRdn;
733 
734 		while (Math.min(name.size(), nameInNamespace.size()) != 0) {
735 			nameRdn = name.getRdn(0);
736 			nameInNamespaceRdn = nameInNamespace.getRdn(0);
737 			if (nameRdn.equals(nameInNamespaceRdn)) {
738 				name.remove(0);
739 				nameInNamespace.remove(0);
740 			} else
741 				break;
742 		}
743 
744 		int innerPosn;
745 		while (Math.min(name.size(), nameInNamespace.size()) != 0) {
746 			innerPosn = nameInNamespace.size() - 1;
747 			nameRdn = name.getRdn(0);
748 			nameInNamespaceRdn = nameInNamespace.getRdn(innerPosn);
749 			if (nameRdn.equals(nameInNamespaceRdn)) {
750 				name.remove(0);
751 				nameInNamespace.remove(innerPosn);
752 			} else
753 				break;
754 		}
755 
756 		return name;
757 	}
758 
759 	protected static class User {
760 
761 		private final GSSName gssName;
762 		private final Sid sid;
763 		private final List<String> roles;
764 		private final Map<String, Object> additionalAttributes;
765 
766 		public User(GSSName gssName, Sid sid, List<String> roles,
767 				Map<String, Object> additionalAttributes) {
768 			this.gssName = gssName;
769 			this.sid = sid;
770 			this.roles = roles;
771 			this.additionalAttributes = additionalAttributes;
772 		}
773 
774 		public GSSName getGssName() {
775 			return gssName;
776 		}
777 
778 		public Sid getSid() {
779 			return sid;
780 		}
781 
782 		public List<String> getRoles() {
783 			return roles;
784 		}
785 
786 		public Map<String, Object> getAdditionalAttributes() {
787 			return additionalAttributes;
788 		}
789 
790 	}
791 
792 }