net.sf.michaelo.tomcat.realm.ActiveDirectoryRealm Maven / Gradle / Ivy
/*
* Copyright 2013–2024 Michael Osipov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sf.michaelo.tomcat.realm;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import javax.naming.CommunicationException;
import javax.naming.CompositeName;
import javax.naming.InvalidNameException;
import javax.naming.Name;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.ReferralException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.ManageReferralControl;
import javax.naming.ldap.Rdn;
import javax.security.auth.x500.X500Principal;
import net.sf.michaelo.dirctxsrc.DirContextSource;
import net.sf.michaelo.tomcat.authenticator.SpnegoAuthenticator;
import net.sf.michaelo.tomcat.realm.asn1.OtherNameAsn1Parser;
import net.sf.michaelo.tomcat.realm.asn1.OtherNameParseResult;
import net.sf.michaelo.tomcat.realm.mapper.SamAccountNameRfc2247Mapper;
import net.sf.michaelo.tomcat.realm.mapper.UserPrincipalNameSearchMapper;
import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper;
import net.sf.michaelo.tomcat.realm.mapper.UsernameSearchMapper.MappedValues;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Server;
import org.apache.catalina.authenticator.SSLAuthenticator;
import org.apache.catalina.realm.CombinedRealm;
import org.apache.commons.lang3.StringUtils;
import org.apache.naming.ContextBindings;
import org.apache.tomcat.util.buf.Asn1Parser;
import org.apache.tomcat.util.collections.SynchronizedStack;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
/**
* A realm which retrieves already authenticated users from Active Directory via LDAP.
*
* Configuration
Following options can be configured:
*
* - {@code dirContextSourceName}: the name of the {@link DirContextSource} in JNDI with which
* principals will be retrieved.
* - {@code localDirContextSource}: whether this {@code DirContextSource} is locally configured in
* the {@code context.xml} or globally configured in the {@code server.xml} (optional). Default
* value is {@code false}.
* - {@code roleFormats}: comma-separated list of role formats to be applied to user security
* groups. The following values are possible: {@code sid} retrieves the {@code objectSid} and
* {@code sIDHistory} attribute values, {@code name} retrieves the {@code msDS-PrincipalName} attribute
* value representing the down-level logon name format:
{netbiosDomain}\{samAccountName}
,
* and {@code nameEx} retrieves the {@code distinguishedName} and {@code sAMAccountName} attribute
* values and converts the DC RDNs from the DN to the Kerberos realm and appends the
* {@code sAMAccountName} (reversed RFC 2247) with format {realm}\{samAccountName}
.
* Default is {@code sid}.
* - {@code prependRoleFormat}: whether the role format is prepended to the role as
*
{roleFormat}:{role}
. Default is {@code false}.
* - {@code additionalAttributes}: comma-separated list of attributes to be retrieved for the
* principal. Binary attributes must end with {@code ;binary} and will be stored as {@code byte[]},
* ordinary attributes will be stored as {@code String}. If an attribute is multivalued, it will be
* stored as {@code List}.
* - {@code connectionPoolSize}: the maximum amount of directory server connections the pool will
* hold. Default is zero which means no connections will be pooled.
* - {@code maxIdleTime}: the maximum amount of time in milliseconds a directory server connection
* should remain idle before it is closed. Default value is 15 minutes.
*
* Connection Pooling
This realm offers a poor man's directory server connection pooling
* which can drastically improve access performance for non-session (stateless) applications. It
* utilizes a LIFO structure based on {@link SynchronizedStack}. No background thread is managing
* the connections. They are acquired, validated, eventually closed and opened when
* {@link #getPrincipal(GSSName, GSSCredential)} is invoked. Validation involves a minimal and
* limited query with at most 500 ms of wait time just to verify the connection is alive and
* healthy. If this query fails, the connection is closed immediately. If the amount of requested
* connections exceeds the ones available in the pool, new ones are opened and pushed onto the pool.
* If the pool does not accept any addtional connections they are closed immediately.
*
* Note: This connection pool feature has to be explicitly enabled by setting
* {@code connectionPoolSize} to greater than zero.
*
*
On Usernames
* This realm processes supplied usernames with different types.
* Supported Types
Only a subset of username types are accepted in contrast to
* other realm implementations. Namely, this realm must know what type is passed to properly map
* it into Active Directory search space with a {@link UsernameSearchMapper} implementation.
* The supported username types are:
*
* - {@link GSSName} by inspecting the string name type,
* - {@link X509Certificate} by extracting the {@code SAN:otherName} field and matching for
* MS UPN type id (1.3.6.1.4.1.311.20.2.3).
*
*
* Note: Both types represent already authenticated users by means of a
* GSS and/or TLScontext.
*
Canonicalization
* This realm will always try to canonicalize a given username type to a real {@link GSSName}
* with the string name type of {@code KRB5_NT_PRINCIPAL} (1.2.840.113554.1.2.2.1) similar to
* the {@code canonicalize} flag in the
* {@code krb5.conf} file. This makes the final {@link GSSName} fully usable in subsequent
* GSS-API calls.
*
* Referral Handling
Active Directory uses two type of responses
* when it cannot complete a search request: referrals and search result references. Both are
* different in nature, read more about them here.
* For this section I will use the term referral for both types synomously as does the
* JNDI/LDAP Provider documentation.
*
* When working with the default LDAP ports (not
* GC) or in a multi-forest environment, it is highly likely to receive referrals (either
* subordinate or cross) during a search or lookup. Sun's JNDI/LDAP Provider takes the following
* approach to handle referrals with the {@code java.naming.referral} property and its values:
* {@code ignore}, {@code throw}, and {@code follow}. You can ignore referrals altogether, but
* the provider will still signal a {@link PartialResultException} when a {@link NamingEnumeration}
* is iterated. The reason is because it adds a {@link ManageReferralControl} when {@code ignore}
* is set and assumes that the target server will ignore referrals, but this is a misconception
* in this provider implementation, see
* here
* and here. It is also unclear
* whether Microsoft Active Directory supports this control.
*
* This realm will catch this exception and continue to process the enumeration. If the {@code DirContextSource}
* is set to {@code throw}, this realm will catch the {@link ReferralException} also, but avoid
* following referrals manually (for several reasons) and will continue with the process.
* Following referrals automatically is a completely opaque operation to the application, no
* {@code ReferralException} is thrown, but the referrals are handled internally and referral
* contexts are queried and closed. If you choose to {@code follow} referrals you must
* use my Active Directory DNS Locator
* otherwise the queries will fail and you will suffer from
* JDK-8161361 and
* JDK-8160768!
*
* Why do you need to use my Active Directory DNS Locator? Microsoft takes a very
* sophisticated approach on not to rely on hostnames because servers can be provisioned and
* decommissioned any time. Instead, they heavily rely on DNS domain names and DNS SRV records
* at runtime. I.e., an initial or a referral URL does not contain a hostname, but only a domain
* name. While you can connect to the service with this name, you cannot easily authenticate
* against it with Kerberos because one cannot bind the same SPN {@code ldap/@},
* e.g., {@code ldap/[email protected]} to more than one account. If you try authenticate
* anyway, you will receive a "Server not found in Kerberos database (7)" error. Therefore, one has
* to perform a DNS SRV query ({@code _ldap._tcp.}) to test whether this name is a
* hostname or a domain name served by one or more servers. If it turns out to be a domain name,
* you have to select one target host from the query response (according to RFC 2782), construct
* a domain-based SPN {@code ldap//@} or a host-based
* one {@code ldap/@}, obtain a service ticket for and connect to that target
* host.
*
* How to handle referrals? There are several ways depending on your setup: Use the
* Global Catalog (port 3268) with a single forest and set referrals to {@code ignore}, or
* with multiple forests and set referrals to either
*
* - {@code follow} with a {@link DirContextSource} in your home forest and use my Active
* Directory DNS Locator, or
* - {@code ignore} with multiple {@code DirContextSources}, and create a {@link CombinedRealm}
* with one {@code ActiveDirectoryRealm} per forest.
*
*
* You will then have the principal properly looked up in Active Directory.
*
* Further references:
* How DNS Support
* for Active Directory Works is a good read on the DNS topic as well as
* Global Catalog and LDAP
* Searches and LDAP
* Referrals.
*
* Note: Always remember, referrals incur an amplification in time and space and
* make the entire process slower.
*
* Tip: Consider using the {@link PacDataActiveDirectoryRealm} if you don't need
* all features and use {@link SpnegoAuthenticator SPNEGO authentication} only since it is orders of
* magnitude faster, but remember though that {@link SSLAuthenticator X.509 authentication} still
* requires this realm.
*
* @see ActiveDirectoryPrincipal
*/
public class ActiveDirectoryRealm extends ActiveDirectoryRealmBase {
// A mere holder class for directory server connections
protected static class DirContextConnection {
protected String id;
protected long lastBorrowTime;
protected DirContext context;
}
private static final AtomicLong COUNT = new AtomicLong(0);
// 1.3.6.1.4.1.311.20.2.3
private static final byte[] MS_UPN_OID_BYTES = { (byte) 0x2B, (byte) 0x06, (byte) 0x01, (byte) 0x04, (byte) 0x01,
(byte) 0x82, (byte) 0x37, (byte) 0x14, (byte) 0x02, (byte) 0x03 };
private static final long UF_ACCOUNT_DISABLE = 0x00000002L;
private static final long UF_NORMAL_ACCOUNT = 0x00000200L;
private static final long UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000L;
private final static Oid MS_UPN;
private final static Oid KRB5_NT_PRINCIPAL;
private final static Map X500_PRINCIPAL_OID_MAP = new HashMap();
private static final UsernameSearchMapper[] USERNAME_SEARCH_MAPPERS = {
new SamAccountNameRfc2247Mapper(), new UserPrincipalNameSearchMapper() };
private static final String[] DEFAULT_USER_ATTRIBUTES = new String[] { "userAccountControl",
"memberOf", "objectSid;binary", "sAMAccountName" };
private static final String[] DEFAULT_ROLE_ATTRIBUTES = new String[] { "groupType" };
private static final String DEFAULT_ROLE_FORMAT = "sid";
private static final Map ROLE_FORMAT_ATTRIBUTES = new HashMap<>();
static {
try {
MS_UPN = new Oid("1.3.6.1.4.1.311.20.2.3");
} catch (GSSException e) {
throw new IllegalStateException("Failed to create OID for MS_UPN");
}
try {
KRB5_NT_PRINCIPAL = new Oid("1.2.840.113554.1.2.2.1");
} catch (GSSException e) {
throw new IllegalStateException("Failed to create OID for KRB5_NT_PRINCIPAL");
}
X500_PRINCIPAL_OID_MAP.put("1.2.840.113549.1.9.1", "emailAddress");
X500_PRINCIPAL_OID_MAP.put("2.5.4.5", "serialNumber");
// surname
X500_PRINCIPAL_OID_MAP.put("2.5.4.4", "SN");
// givenName
X500_PRINCIPAL_OID_MAP.put("2.5.4.42", "GN");
ROLE_FORMAT_ATTRIBUTES.put("sid", new String[] { "objectSid;binary", "sIDHistory;binary" });
ROLE_FORMAT_ATTRIBUTES.put("name", new String [] { "msDS-PrincipalName" } );
ROLE_FORMAT_ATTRIBUTES.put("nameEx", new String [] { "distinguishedName", "sAMAccountName" } );
}
protected boolean localDirContextSource;
protected String dirContextSourceName;
protected String[] attributes;
protected String[] additionalAttributes;
protected String[] roleFormats;
protected String[] roleAttributes;
protected boolean prependRoleFormat;
protected int connectionPoolSize = 0;
protected long maxIdleTime = 900_000L;
// Poor man's connection pool
protected SynchronizedStack connectionPool;
protected static String getNextConnectionId() {
return String.format("conn-%06d", COUNT.incrementAndGet());
}
/**
* Sets whether the {@code DirContextSource} is locally ({@code context.xml} defined or globally
* {@code server.xml}.
*
* @param localDirContextSource
* the local directory context source indication
*/
public void setLocalDirContextSource(boolean localDirContextSource) {
this.localDirContextSource = localDirContextSource;
}
/**
* Sets the name of the {@code DirContextSource}
*
* @param dirContextSourceName
* the directory context source name
*/
public void setDirContextSourceName(String dirContextSourceName) {
this.dirContextSourceName = dirContextSourceName;
}
/**
* Sets a comma-separated list of Active Directory attributes retreived and stored for the user
* principal.
*
* @param additionalAttributes
* the additional attributes
*/
public void setAdditionalAttributes(String additionalAttributes) {
this.additionalAttributes = additionalAttributes.split(",");
this.attributes = new String[DEFAULT_USER_ATTRIBUTES.length + this.additionalAttributes.length];
System.arraycopy(DEFAULT_USER_ATTRIBUTES, 0, this.attributes, 0,
DEFAULT_USER_ATTRIBUTES.length);
System.arraycopy(this.additionalAttributes, 0, this.attributes, DEFAULT_USER_ATTRIBUTES.length,
this.additionalAttributes.length);
}
/**
* Sets a comma-separated list of role formats to be applied to user security groups
* from Active Directory.
*
* @param roleFormats the role formats
*/
public void setRoleFormats(String roleFormats) {
this.roleFormats = roleFormats.split(",");
List attributes = new ArrayList<>(Arrays.asList(DEFAULT_ROLE_ATTRIBUTES));
for (String roleFormat : this.roleFormats) {
if (ROLE_FORMAT_ATTRIBUTES.get(roleFormat) != null)
attributes.addAll(Arrays.asList(ROLE_FORMAT_ATTRIBUTES.get(roleFormat)));
}
this.roleAttributes = attributes.toArray(new String[0]);
}
/**
* Sets whether the role format is prepended to the role.
*
* @param prependRoleFormat
* the prepend role format indication
*/
public void setPrependRoleFormat(boolean prependRoleFormat) {
this.prependRoleFormat = prependRoleFormat;
}
/**
* Sets the maximum amount of directory server connections the pool will hold.
*
* @param connectionPoolSize
* the connection pool size
*/
public void setConnectionPoolSize(int connectionPoolSize) {
this.connectionPoolSize = connectionPoolSize;
}
/**
* Sets the maximum amount of time in milliseconds a directory server connection should remain
* idle before it is closed.
*
* @param maxIdleTime
* the maximum idle time
*/
public void setMaxIdleTime(long maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
@Override
protected Principal getPrincipal(X509Certificate userCert) {
try {
Collection> san = userCert.getSubjectAlternativeNames();
if (san == null || san.isEmpty())
return null;
String dn = userCert.getSubjectX500Principal().getName(X500Principal.RFC2253, X500_PRINCIPAL_OID_MAP);
for (List> sanField : san) {
Integer nameType = (Integer) sanField.get(0);
// SAN's OtherName, see X509Certificate#getSubjectAlternativeNames() Javadoc
if (nameType == 0) {
byte[] otherName = (byte[]) sanField.get(1);
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.processingSanOtherName",
Base64.getEncoder().encodeToString(otherName), dn));
try {
OtherNameParseResult result = OtherNameAsn1Parser.parse(otherName);
if (Arrays.equals(result.getTypeId(), MS_UPN_OID_BYTES)) {
Asn1Parser parser = new Asn1Parser(result.getValue());
String upn = parser.parseUTF8String();
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.msUpnExtracted", upn, dn));
GSSName gssName = new StubGSSName(upn, MS_UPN);
return getPrincipal(gssName, null, true);
}
} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
logger.warn(sm.getString("activeDirectoryRealm.sanOtherNameParsingFailed"), e);
}
}
}
} catch (CertificateParsingException e) {
logger.warn(sm.getString("activeDirectoryRealm.sanParsingFailed"), e);
}
return null;
}
@Override
protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) {
if (gssName.isAnonymous())
return new ActiveDirectoryPrincipal(gssName, Sid.ANONYMOUS_SID, gssCredential);
return getPrincipal(gssName, gssCredential, true);
}
protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential, boolean retry) {
DirContextConnection connection = null;
try {
connection = acquire();
try {
User user = getUser(connection.context, gssName);
if (user != null) {
List roles = getRoles(connection.context, user);
return new ActiveDirectoryPrincipal(user.getGssName(), user.getSid(), roles, gssCredential,
user.getAdditionalAttributes());
}
} catch (NamingException e) {
// This construct is an ugly hack for
// https://bugs.openjdk.java.net/browse/JDK-8273402
boolean canRetry = false;
if (e instanceof CommunicationException || e instanceof ServiceUnavailableException)
canRetry = true;
else {
String explanation = e.getExplanation();
if (explanation.equals("LDAP connection has been closed")
|| explanation.startsWith("LDAP response read timed out, timeout used:"))
canRetry = true;
}
if (retry && canRetry) {
logger.error(sm.getString("activeDirectoryRealm.principalSearchFailed.retry", gssName), e);
close(connection);
return getPrincipal(gssName, gssCredential, false);
}
logger.error(sm.getString("activeDirectoryRealm.principalSearchFailed", gssName), e);
close(connection);
}
} catch (NamingException e) {
logger.error(sm.getString("activeDirectoryRealm.acquire.namingException"), e);
} finally {
release(connection);
}
return null;
}
protected DirContextConnection acquire() throws NamingException {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.acquire"));
DirContextConnection connection = null;
while (connection == null) {
connection = connectionPool.pop();
if (connection != null) {
long idleTime = System.currentTimeMillis() - connection.lastBorrowTime;
// TODO support maxIdleTime = -1 (no expiry)
if (idleTime > maxIdleTime) {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.exceedMaxIdleTime", connection.id));
close(connection);
connection = null;
} else {
boolean valid = validate(connection);
if (valid) {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.reuse", connection.id));
} else {
close(connection);
connection = null;
}
}
} else {
connection = new DirContextConnection();
open(connection);
}
}
connection.lastBorrowTime = System.currentTimeMillis();
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.acquired", connection.id));
return connection;
}
protected boolean validate(DirContextConnection connection) {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.validate", connection.id));
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.OBJECT_SCOPE);
controls.setCountLimit(1);
controls.setReturningAttributes(new String[] { "objectClass" });
// This applies to a server-side time limit which is actually translated to a second resolution and
// does not apply to a socket read timeout
controls.setTimeLimit(500);
NamingEnumeration results = null;
try {
results = connection.context.search("", "objectclass=*", controls);
if (results.hasMore())
return true;
} catch (NamingException e) {
logger.error(sm.getString("activeDirectoryRealm.validate.namingException", connection.id), e);
} finally {
close(results);
}
return false;
}
protected void release(DirContextConnection connection) {
if (connection == null)
return;
if (connection.context == null)
return;
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.release", connection.id));
if (!connectionPool.push(connection))
close(connection);
}
protected void open(DirContextConnection connection) throws NamingException {
javax.naming.Context context = null;
if (localDirContextSource) {
context = ContextBindings.getClassLoader();
context = (javax.naming.Context) context.lookup("comp/env");
} else {
Server server = getServer();
context = server.getGlobalNamingContext();
}
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.open"));
DirContextSource contextSource = (DirContextSource) context
.lookup(dirContextSourceName);
connection.context = contextSource.getDirContext();
connection.id = getNextConnectionId();
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.opened", connection.id));
}
protected void close(DirContextConnection connection) {
if (connection.context == null)
return;
try {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.close", connection.id));
connection.context.close();
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.closed", connection.id));
} catch (NamingException e) {
logger.error(sm.getString("activeDirectoryRealm.close.namingException", connection.id), e);
}
connection.context = null;
}
protected void close(NamingEnumeration> results) {
if (results == null)
return;
try {
results.close();
} catch (NamingException e) {
; // Ignore
}
}
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
if (attributes == null)
attributes = DEFAULT_USER_ATTRIBUTES;
if (roleFormats == null)
setRoleFormats(DEFAULT_ROLE_FORMAT);
}
@Override
protected void startInternal() throws LifecycleException {
connectionPool = new SynchronizedStack<>(connectionPoolSize, connectionPoolSize);
DirContextConnection connection = null;
try {
connection = acquire();
try {
String referral = (String) connection.context.getEnvironment().get(DirContext.REFERRAL);
if ("follow".equals(referral))
logger.warn(sm.getString("activeDirectoryRealm.referralFollow"));
} catch (NamingException e) {
logger.error(sm.getString("activeDirectoryRealm.environmentFailed"), e);
close(connection);
}
} catch (NamingException e) {
logger.error(sm.getString("activeDirectoryRealm.acquire.namingException"), e);
} finally {
release(connection);
}
super.startInternal();
}
@Override
protected void stopInternal() throws LifecycleException {
super.stopInternal();
DirContextConnection connection = null;
while ((connection = connectionPool.pop()) != null)
close(connection);
connectionPool = null;
}
private Oid getStringNameType(GSSName gssName) {
try {
return gssName.getStringNameType();
} catch (GSSException e) {
return null;
}
}
private String toRealm(Name distinguishedName) {
LdapName dn = (LdapName) distinguishedName;
StringBuilder realm = new StringBuilder();
for(Rdn rdn : dn.getRdns())
if (rdn.getType().equalsIgnoreCase("DC"))
realm.insert(0, ((String) rdn.getValue()).toUpperCase(Locale.ROOT) + ".");
if (realm.length() > 0)
realm.deleteCharAt(realm.length() - 1);
return realm.toString();
}
protected User getUser(DirContext context, GSSName gssName) throws NamingException {
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(attributes);
String searchFilter;
Name searchBase = null;
String searchAttributeName;
String searchAttributeValue;
MappedValues mappedValues;
NamingEnumeration results = null;
for (UsernameSearchMapper mapper : USERNAME_SEARCH_MAPPERS) {
String mapperClassName = mapper.getClass().getSimpleName();
if (!mapper.supportsGssName(gssName)) {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.nameTypeNotSupported", mapperClassName,
getStringNameType(gssName), gssName));
continue;
}
mappedValues = mapper.map(context, gssName);
searchBase = getRelativeName(context, mappedValues.getSearchBase());
searchAttributeName = mappedValues.getSearchAttributeName();
searchAttributeValue = mappedValues.getSearchUsername();
searchFilter = String.format("(%s={0})", searchAttributeName);
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.usernameSearch",
searchAttributeValue, searchBase, searchAttributeName, mapperClassName));
try {
results = context.search(searchBase, searchFilter,
new Object[] { searchAttributeValue }, searchCtls);
} catch (ReferralException e) {
logger.warn(sm.getString("activeDirectoryRealm.user.referralException",
mapperClassName, e.getRemainingName(), e.getReferralInfo()));
continue;
}
try {
if (!results.hasMore()) {
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.userNotFoundWithMapper", gssName,
mapperClassName));
close(results);
} else
break;
} catch (PartialResultException e) {
logger.debug(sm.getString("activeDirectoryRealm.user.partialResultException",
mapperClassName, e.getRemainingName()));
close(results);
}
}
if (results == null || !results.hasMore()) {
logger.debug(sm.getString("activeDirectoryRealm.userNotFound", gssName));
close(results);
return null;
}
SearchResult result = results.next();
try {
if (results.hasMore()) {
logger.error(sm.getString("activeDirectoryRealm.duplicateUser", gssName));
close(results);
return null;
}
} catch (ReferralException e) {
logger.warn(sm.getString("activeDirectoryRealm.duplicateUser.referralException", gssName,
e.getRemainingName(), e.getReferralInfo()));
} catch (PartialResultException e) {
logger.debug(sm.getString("activeDirectoryRealm.duplicateUser.partialResultException", gssName,
e.getRemainingName()));
}
close(results);
Attributes userAttributes = result.getAttributes();
long userAccountControl = Long
.parseLong((String) userAttributes.get("userAccountControl").get());
if ((userAccountControl & UF_ACCOUNT_DISABLE) != 0L) {
logger.warn(sm.getString("activeDirectoryRealm.userFoundButDisabled", gssName));
return null;
}
if ((userAccountControl & UF_NORMAL_ACCOUNT) == 0L && (userAccountControl & UF_WORKSTATION_TRUST_ACCOUNT) == 0L) {
logger.warn(sm.getString("activeDirectoryRealm.userFoundButNotSupported", gssName));
return null;
}
Name dn = getDistinguishedName(context, searchBase, result);
byte[] sidBytes = (byte[]) userAttributes.get("objectSid;binary").get();
Sid sid = new Sid(sidBytes);
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.userFound", gssName, dn, sid));
if (!KRB5_NT_PRINCIPAL.equals(getStringNameType(gssName))) {
String samAccountName = (String) userAttributes.get("sAMAccountName").get();
String realm = toRealm(dn);
String krb5Principal = samAccountName + "@" + realm;
if (logger.isTraceEnabled())
logger.trace(sm.getString("activeDirectoryRealm.canonicalizingUser", getStringNameType(gssName),
KRB5_NT_PRINCIPAL));
GSSName canonGssName = null;
try {
canonGssName = GSSManager.getInstance().createName(krb5Principal, KRB5_NT_PRINCIPAL);
} catch (GSSException e) {
logger.warn(sm.getString("activeDirectoryRealm.canonicalizeUserFailed", gssName));
return null;
}
if (logger.isDebugEnabled())
logger.debug(sm.getString("activeDirectoryRealm.userCanonicalized", canonGssName));
gssName = canonGssName;
}
Attribute memberOfAttr = userAttributes.get("memberOf");
List memberOfs = new LinkedList();
if (memberOfAttr != null && memberOfAttr.size() > 0) {
NamingEnumeration> memberOfValues = memberOfAttr.getAll();
while (memberOfValues.hasMore())
memberOfs.add((String) memberOfValues.next());
close(memberOfValues);
}
Map additionalAttributesMap = Collections.emptyMap();
if (additionalAttributes != null && additionalAttributes.length > 0) {
additionalAttributesMap = new HashMap();
for (String addAttr : additionalAttributes) {
Attribute attr = userAttributes.get(addAttr);
if (attr != null && attr.size() > 0) {
if (attr.size() > 1) {
List