diff --git a/dev-tools/checkstyle_suppressions.xml b/dev-tools/checkstyle_suppressions.xml index f087898e66c..e84513c6f27 100644 --- a/dev-tools/checkstyle_suppressions.xml +++ b/dev-tools/checkstyle_suppressions.xml @@ -475,12 +475,9 @@ - - - diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java index 60bd3d82edf..4a0d44a452f 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java @@ -33,22 +33,26 @@ class ActiveDirectoryGroupsResolver implements GroupsResolver { private static final String TOKEN_GROUPS = "tokenGroups"; private final String baseDn; private final LdapSearchScope scope; + private final boolean ignoreReferralErrors; - ActiveDirectoryGroupsResolver(Settings settings, String baseDnDefault) { + ActiveDirectoryGroupsResolver(Settings settings, String baseDnDefault, + boolean ignoreReferralErrors) { this.baseDn = settings.get("base_dn", baseDnDefault); this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE); + this.ignoreReferralErrors = ignoreReferralErrors; } @Override public void resolve(LDAPInterface connection, String userDn, TimeValue timeout, Logger logger, Collection attributes, ActionListener> listener) { buildGroupQuery(connection, userDn, timeout, - ActionListener.wrap((filter) -> { + ignoreReferralErrors, ActionListener.wrap((filter) -> { if (filter == null) { listener.onResponse(Collections.emptyList()); } else { logger.debug("group SID to DN [{}] search filter: [{}]", userDn, filter); - search(connection, baseDn, scope.scope(), filter, Math.toIntExact(timeout.seconds()), + search(connection, baseDn, scope.scope(), filter, + Math.toIntExact(timeout.seconds()), ignoreReferralErrors, ActionListener.wrap((results) -> { List groups = results.stream() .map(SearchResultEntry::getDN) @@ -67,8 +71,10 @@ class ActiveDirectoryGroupsResolver implements GroupsResolver { return null; } - static void buildGroupQuery(LDAPInterface connection, String userDn, TimeValue timeout, ActionListener listener) { - searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), + static void buildGroupQuery(LDAPInterface connection, String userDn, TimeValue timeout, + boolean ignoreReferralErrors, ActionListener listener) { + searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, + Math.toIntExact(timeout.seconds()), ignoreReferralErrors, ActionListener.wrap((entry) -> { if (entry == null || entry.hasAttribute(TOKEN_GROUPS) == false) { listener.onResponse(null); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java index a523f7abc54..e5ffd56238a 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java @@ -65,13 +65,18 @@ class ActiveDirectorySessionFactory extends SessionFactory { Settings settings = config.settings(); String domainName = settings.get(AD_DOMAIN_NAME_SETTING); if (domainName == null) { - throw new IllegalArgumentException("missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory"); + throw new IllegalArgumentException("missing [" + AD_DOMAIN_NAME_SETTING + + "] setting for active directory"); } String domainDN = buildDnFromDomain(domainName); - GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN); - defaultADAuthenticator = new DefaultADAuthenticator(settings, timeout, logger, groupResolver, domainDN); - downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout, logger, groupResolver, domainDN, sslService); - upnADAuthenticator = new UpnADAuthenticator(settings, timeout, logger, groupResolver, domainDN); + GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver( + settings.getAsSettings("group_search"), domainDN, ignoreReferralErrors); + defaultADAuthenticator = new DefaultADAuthenticator(settings, timeout, + ignoreReferralErrors, logger, groupResolver, domainDN); + downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout, + ignoreReferralErrors, logger, groupResolver, domainDN, sslService); + upnADAuthenticator = new UpnADAuthenticator(settings, timeout, + ignoreReferralErrors, logger, groupResolver, domainDN); } @Override @@ -135,13 +140,16 @@ class ActiveDirectorySessionFactory extends SessionFactory { abstract static class ADAuthenticator { final TimeValue timeout; + final boolean ignoreReferralErrors; final Logger logger; final GroupsResolver groupsResolver; final String userSearchDN; final LdapSearchScope userSearchScope; - ADAuthenticator(Settings settings, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN) { + ADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors, + Logger logger, GroupsResolver groupsResolver, String domainDN) { this.timeout = timeout; + this.ignoreReferralErrors = ignoreReferralErrors; this.logger = logger; this.groupsResolver = groupsResolver; userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN); @@ -195,19 +203,22 @@ class ActiveDirectorySessionFactory extends SessionFactory { final String userSearchFilter; final String domainName; - DefaultADAuthenticator(Settings settings, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN) { - super(settings, timeout, logger, groupsResolver, domainDN); + DefaultADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors, + Logger logger, GroupsResolver groupsResolver, String domainDN) { + super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN); domainName = settings.get(AD_DOMAIN_NAME_SETTING); userSearchFilter = settings.get(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})" + "(userPrincipalName={0}@" + domainName + ")))"); } @Override - void searchForDN(LDAPConnection connection, String username, SecuredString password, int timeLimitSeconds, - ActionListener listener) { + void searchForDN(LDAPConnection connection, String username, SecuredString password, + int timeLimitSeconds, ActionListener listener) { try { - searchForEntry(connection, userSearchDN, userSearchScope.scope(), createFilter(userSearchFilter, username), - timeLimitSeconds, listener, attributesToSearchFor(groupsResolver.attributes())); + searchForEntry(connection, userSearchDN, userSearchScope.scope(), + createFilter(userSearchFilter, username), timeLimitSeconds, + ignoreReferralErrors, listener, + attributesToSearchFor(groupsResolver.attributes())); } catch (LDAPException e) { listener.onFailure(e); } @@ -220,8 +231,8 @@ class ActiveDirectorySessionFactory extends SessionFactory { } /** - * Active Directory calls the format DOMAIN\\username down-level credentials and this class contains the logic necessary - * to authenticate this form of a username + * Active Directory calls the format DOMAIN\\username down-level credentials and + * this class contains the logic necessary to authenticate this form of a username */ static class DownLevelADAuthenticator extends ADAuthenticator { Cache domainNameCache = CacheBuilder.builder().setMaximumWeight(100).build(); @@ -231,9 +242,12 @@ class ActiveDirectorySessionFactory extends SessionFactory { final SSLService sslService; final RealmConfig config; - DownLevelADAuthenticator(RealmConfig config, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN, + DownLevelADAuthenticator(RealmConfig config, TimeValue timeout, + boolean ignoreReferralErrors, Logger logger, + GroupsResolver groupsResolver, String domainDN, SSLService sslService) { - super(config.settings(), timeout, logger, groupsResolver, domainDN); + super(config.settings(), timeout, ignoreReferralErrors, logger, groupsResolver, + domainDN); this.domainDN = domainDN; this.settings = config.settings(); this.sslService = sslService; @@ -255,8 +269,9 @@ class ActiveDirectorySessionFactory extends SessionFactory { } else { try { searchForEntry(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), - createFilter("(&(objectClass=user)(sAMAccountName={0}))", accountName), timeLimitSeconds, listener, - attributesToSearchFor(groupsResolver.attributes())); + createFilter("(&(objectClass=user)(sAMAccountName={0}))", + accountName), timeLimitSeconds, ignoreReferralErrors, + listener, attributesToSearchFor(groupsResolver.attributes())); } catch (LDAPException e) { IOUtils.closeWhileHandlingException(connection); listener.onFailure(e); @@ -274,8 +289,9 @@ class ActiveDirectorySessionFactory extends SessionFactory { if (cachedName != null) { listener.onResponse(cachedName); } else if (usingGlobalCatalog(settings, connection)) { - // the global catalog does not replicate the necessary information to map a netbios dns name to a DN so we need to instead - // connect to the normal ports. This code uses the standard ports to avoid adding even more settings and is probably ok as + // the global catalog does not replicate the necessary information to map a netbios + // dns name to a DN so we need to instead connect to the normal ports. This code + // uses the standard ports to avoid adding even more settings and is probably ok as // most AD users do not use non-standard ports final LDAPConnectionOptions options = connectionOptions(config, sslService, logger); boolean startedSearching = false; @@ -283,7 +299,8 @@ class ActiveDirectorySessionFactory extends SessionFactory { try { Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); if (connection.getSSLSession() != null) { - searchConnection = LdapUtils.privilegedConnect(() -> new LDAPConnection(connection.getSocketFactory(), options, + searchConnection = LdapUtils.privilegedConnect( + () -> new LDAPConnection(connection.getSocketFactory(), options, connection.getConnectedAddress(), 636)); } else { searchConnection = LdapUtils.privilegedConnect(() -> @@ -291,14 +308,16 @@ class ActiveDirectorySessionFactory extends SessionFactory { } searchConnection.bind(username, new String(password.internalChars())); final LDAPConnection finalConnection = searchConnection; - search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, - ActionListener.wrap((results) -> { - IOUtils.close(finalConnection); - handleSearchResults(results, netBiosDomainName, domainNameCache, listener); - }, (e) -> { - IOUtils.closeWhileHandlingException(connection); - listener.onFailure(e); - }), + search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, + timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap( + (results) -> { + IOUtils.close(finalConnection); + handleSearchResults(results, netBiosDomainName, + domainNameCache, listener); + }, (e) -> { + IOUtils.closeWhileHandlingException(connection); + listener.onFailure(e); + }), "ncname"); startedSearching = true; } catch (LDAPException e) { @@ -311,8 +330,10 @@ class ActiveDirectorySessionFactory extends SessionFactory { } else { try { Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); - search(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, - ActionListener.wrap((results) -> handleSearchResults(results, netBiosDomainName, domainNameCache, listener), + search(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, + timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap( + (results) -> handleSearchResults(results, netBiosDomainName, + domainNameCache, listener), (e) -> { IOUtils.closeWhileHandlingException(connection); listener.onFailure(e); @@ -324,9 +345,12 @@ class ActiveDirectorySessionFactory extends SessionFactory { } } - static void handleSearchResults(List results, String netBiosDomainName, Cache domainNameCache, + static void handleSearchResults(List results, String netBiosDomainName, + Cache domainNameCache, ActionListener listener) { - Optional entry = results.stream().filter((r) -> r.hasAttribute("ncname")).findFirst(); + Optional entry = results.stream() + .filter((r) -> r.hasAttribute("ncname")) + .findFirst(); if (entry.isPresent()) { final String value = entry.get().getAttributeValue("ncname"); try { @@ -353,8 +377,9 @@ class ActiveDirectorySessionFactory extends SessionFactory { private static final String UPN_USER_FILTER = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={1})))"; - UpnADAuthenticator(Settings settings, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN) { - super(settings, timeout, logger, groupsResolver, domainDN); + UpnADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors, + Logger logger, GroupsResolver groupsResolver, String domainDN) { + super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN); } void searchForDN(LDAPConnection connection, String username, SecuredString password, int timeLimitSeconds, @@ -366,7 +391,8 @@ class ActiveDirectorySessionFactory extends SessionFactory { final String domainDN = buildDnFromDomain(domainName); try { Filter filter = createFilter(UPN_USER_FILTER, accountName, username); - searchForEntry(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, listener, + searchForEntry(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, + timeLimitSeconds, ignoreReferralErrors, listener, attributesToSearchFor(groupsResolver.attributes())); } catch (LDAPException e) { listener.onFailure(e); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java index c346f0b7c2e..b532ac0faa9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java @@ -16,7 +16,6 @@ import com.unboundid.ldap.sdk.ServerSet; import com.unboundid.ldap.sdk.SimpleBindRequest; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.IOUtils; -import org.elasticsearch.SpecialPermission; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; @@ -294,7 +293,8 @@ class LdapUserSearchSessionFactory extends SessionFactory { private void findUser(String user, LDAPInterface ldapInterface, ActionListener listener) { searchForEntry(ldapInterface, userSearchBaseDn, scope.scope(), - createEqualityFilter(userAttribute, encodeValue(user)), Math.toIntExact(timeout.seconds()), listener, + createEqualityFilter(userAttribute, encodeValue(user)), + Math.toIntExact(timeout.seconds()), ignoreReferralErrors, listener, attributesToSearchFor(groupResolver.attributes())); } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolver.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolver.java index 18c0ceedfd0..08d3ea8bc1d 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolver.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolver.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver; +import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; import java.util.Collection; import java.util.Collections; @@ -33,29 +34,34 @@ import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.OBJE import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.createFilter; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.search; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry; +import static org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory.IGNORE_REFERRAL_ERRORS_SETTING; /** - * Resolves the groups for a user by executing a search with a filter usually that contains a group object class with a attribute that - * matches an ID of the user + * Resolves the groups for a user by executing a search with a filter usually that contains a group + * object class with a attribute that matches an ID of the user */ class SearchGroupsResolver implements GroupsResolver { private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" + - "(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group)(objectclass=posixGroup))" + + "(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)" + + "(objectclass=group)(objectclass=posixGroup))" + "(|(uniqueMember={0})(member={0})(memberUid={0})))"; - static final Setting BASE_DN = Setting.simpleString("group_search.base_dn", Setting.Property.NodeScope); + static final Setting BASE_DN = Setting.simpleString("group_search.base_dn", + Setting.Property.NodeScope); static final Setting SCOPE = new Setting<>("group_search.scope", (String) null, s -> LdapSearchScope.resolve(s, LdapSearchScope.SUB_TREE), Setting.Property.NodeScope); - static final Setting USER_ATTRIBUTE = Setting.simpleString("group_search.user_attribute", Setting.Property.NodeScope); + static final Setting USER_ATTRIBUTE = Setting.simpleString( + "group_search.user_attribute", Setting.Property.NodeScope); - static final Setting FILTER = new Setting<>("group_search.filter", GROUP_SEARCH_DEFAULT_FILTER, - Function.identity(), Setting.Property.NodeScope); + static final Setting FILTER = new Setting<>("group_search.filter", + GROUP_SEARCH_DEFAULT_FILTER, Function.identity(), Setting.Property.NodeScope); private final String baseDn; private final String filter; private final String userAttribute; private final LdapSearchScope scope; + private final boolean ignoreReferralErrors; SearchGroupsResolver(Settings settings) { if (BASE_DN.exists(settings)) { @@ -66,6 +72,7 @@ class SearchGroupsResolver implements GroupsResolver { filter = FILTER.get(settings); userAttribute = USER_ATTRIBUTE.get(settings); scope = SCOPE.get(settings); + this.ignoreReferralErrors = IGNORE_REFERRAL_ERRORS_SETTING.get(settings); } @Override @@ -77,9 +84,14 @@ class SearchGroupsResolver implements GroupsResolver { } else { try { Filter userFilter = createFilter(filter, userId); - search(connection, baseDn, scope.scope(), userFilter, Math.toIntExact(timeout.seconds()), + search(connection, baseDn, scope.scope(), userFilter, + Math.toIntExact(timeout.seconds()), ignoreReferralErrors, ActionListener.wrap( - (results) -> listener.onResponse(results.stream().map((r) -> r.getDN()).collect(Collectors.toList())), + (results) -> listener.onResponse(results + .stream() + .map((r) -> r.getDN()) + .collect(Collectors.toList()) + ), listener::onFailure), SearchRequest.NO_ATTRIBUTES); } catch (LDAPException e) { @@ -96,12 +108,13 @@ class SearchGroupsResolver implements GroupsResolver { return null; } - private void getUserId(String dn, Collection attributes, LDAPInterface connection, TimeValue timeout, - ActionListener listener) { + private void getUserId(String dn, Collection attributes, LDAPInterface connection, + TimeValue timeout, ActionListener listener) { if (isNullOrEmpty(userAttribute)) { listener.onResponse(dn); } else if (attributes != null) { - final String value = attributes.stream().filter((attribute) -> attribute.getName().equals(userAttribute)) + final String value = attributes.stream() + .filter((attribute) -> attribute.getName().equals(userAttribute)) .map(Attribute::getValue) .findFirst() .orElse(null); @@ -111,8 +124,10 @@ class SearchGroupsResolver implements GroupsResolver { } } - void readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout, ActionListener listener) { - searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), + void readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout, + ActionListener listener) { + searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, + Math.toIntExact(timeout.seconds()), ignoreReferralErrors, ActionListener.wrap((entry) -> { if (entry == null || entry.hasAttribute(userAttribute) == false) { listener.onResponse(null); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolver.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolver.java index 27ac1b57491..c492a344f72 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolver.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/UserAttributeGroupsResolver.java @@ -26,6 +26,7 @@ import java.util.function.Function; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.OBJECT_CLASS_PRESENCE_FILTER; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry; +import static org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory.IGNORE_REFERRAL_ERRORS_SETTING; /** * Resolves the groups of a user based on the value of a attribute of the user's ldap entry @@ -35,13 +36,15 @@ class UserAttributeGroupsResolver implements GroupsResolver { private static final Setting ATTRIBUTE = new Setting<>("user_group_attribute", "memberOf", Function.identity(), Setting.Property.NodeScope); private final String attribute; + private final boolean ignoreReferralErrors; UserAttributeGroupsResolver(Settings settings) { - this(ATTRIBUTE.get(settings)); + this(ATTRIBUTE.get(settings), IGNORE_REFERRAL_ERRORS_SETTING.get(settings)); } - private UserAttributeGroupsResolver(String attribute) { + private UserAttributeGroupsResolver(String attribute, boolean ignoreReferralErrors) { this.attribute = Objects.requireNonNull(attribute); + this.ignoreReferralErrors = ignoreReferralErrors; } @Override @@ -53,7 +56,7 @@ class UserAttributeGroupsResolver implements GroupsResolver { listener.onResponse(Collections.unmodifiableList(list)); } else { searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), - ActionListener.wrap((entry) -> { + ignoreReferralErrors, ActionListener.wrap((entry) -> { if (entry == null || entry.hasAttribute(attribute) == false) { listener.onResponse(Collections.emptyList()); } else { diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java index 555d476eaaf..3417d157fe2 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java @@ -44,12 +44,14 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; -import java.util.function.BiConsumer; import java.util.stream.Collectors; public final class LdapUtils { - public static final Filter OBJECT_CLASS_PRESENCE_FILTER = Filter.createPresenceFilter("objectClass"); + public static final Filter OBJECT_CLASS_PRESENCE_FILTER = + Filter.createPresenceFilter("objectClass"); + + private static final Logger LOGGER = ESLoggerFactory.getLogger(LdapUtils.class); private LdapUtils() { } @@ -62,7 +64,8 @@ public final class LdapUtils { } } - public static T privilegedConnect(CheckedSupplier supplier) throws LDAPException { + public static T privilegedConnect(CheckedSupplier supplier) + throws LDAPException { SpecialPermission.check(); try { return AccessController.doPrivileged((PrivilegedExceptionAction) supplier::get); @@ -83,27 +86,40 @@ public final class LdapUtils { /** * This method performs an asynchronous ldap search operation that could have multiple results */ - public static void searchForEntry(LDAPInterface ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener listener, String... attributes) { + public static void searchForEntry(LDAPInterface ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener listener, + String... attributes) { if (ldap instanceof LDAPConnection) { - searchForEntry((LDAPConnection) ldap, baseDN, scope, filter, timeLimitSeconds, listener, attributes); + searchForEntry((LDAPConnection) ldap, baseDN, scope, filter, timeLimitSeconds, + ignoreReferralErrors, listener, attributes); } else if (ldap instanceof LDAPConnectionPool) { - searchForEntry((LDAPConnectionPool) ldap, baseDN, scope, filter, timeLimitSeconds, listener, attributes); + searchForEntry((LDAPConnectionPool) ldap, baseDN, scope, filter, timeLimitSeconds, + ignoreReferralErrors, listener, attributes); } else { throw new IllegalArgumentException("unsupported LDAPInterface implementation: " + ldap); } } /** - * This method performs an asynchronous ldap search operation that only expects at most one result. If more than one result is found - * then this is an error. If no results are found, then {@code null} will be returned. + * This method performs an asynchronous ldap search operation that only expects at most one + * result. + * If more than one result is found then this is an error + * If no results are found, then {@code null} will be returned. + * If the LDAP server returns an error {@link ResultCode} then this is handled as a + * {@link ActionListener#onFailure(Exception) failure} */ - public static void searchForEntry(LDAPConnection ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener listener, String... attributes) { - LdapSearchResultListener searchResultListener = new SingleEntryListener(ldap, listener, filter); + public static void searchForEntry(LDAPConnection ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener listener, + String... attributes) { + LdapSearchResultListener searchResultListener = new SingleEntryListener(ldap, listener, + filter, ignoreReferralErrors); try { - SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, - false, filter, attributes); + SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, + DereferencePolicy.NEVER, 0, timeLimitSeconds, false, filter, attributes); searchResultListener.setSearchRequest(request); ldap.asyncSearch(request); } catch (LDAPException e) { @@ -112,25 +128,35 @@ public final class LdapUtils { } /** - * This method performs an asynchronous ldap search operation that only expects at most one result. If more than one result is found - * then this is an error. If no results are found, then {@code null} will be returned. + * This method performs an asynchronous ldap search operation that only expects at most one + * result. + * If more than one result is found then this is an error. + * If no results are found, then {@code null} will be returned. + * If the LDAP server returns an error {@link ResultCode} then this is handled as a + * {@link ActionListener#onFailure(Exception) failure} */ - public static void searchForEntry(LDAPConnectionPool ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener listener, String... attributes) { + public static void searchForEntry(LDAPConnectionPool ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener listener, + String... attributes) { boolean searching = false; LDAPConnection ldapConnection = null; try { ldapConnection = privilegedConnect(ldap::getConnection); final LDAPConnection finalConnection = ldapConnection; - searchForEntry(finalConnection, baseDN, scope, filter, timeLimitSeconds, ActionListener.wrap( - (entry) -> { - IOUtils.close(() -> ldap.releaseConnection(finalConnection)); - listener.onResponse(entry); - }, - (e) -> { - IOUtils.closeWhileHandlingException(() -> ldap.releaseConnection(finalConnection)); - listener.onFailure(e); - }), attributes); + searchForEntry(finalConnection, baseDN, scope, filter, timeLimitSeconds, + ignoreReferralErrors, ActionListener.wrap( + (entry) -> { + IOUtils.close(() -> ldap.releaseConnection(finalConnection)); + listener.onResponse(entry); + }, + (e) -> { + IOUtils.closeWhileHandlingException( + () -> ldap.releaseConnection(finalConnection) + ); + listener.onFailure(e); + }), attributes); searching = true; } catch (LDAPException e) { listener.onFailure(e); @@ -145,12 +171,17 @@ public final class LdapUtils { /** * This method performs an asynchronous ldap search operation that could have multiple results */ - public static void search(LDAPInterface ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener> listener, String... attributes) { + public static void search(LDAPInterface ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener> listener, + String... attributes) { if (ldap instanceof LDAPConnection) { - search((LDAPConnection) ldap, baseDN, scope, filter, timeLimitSeconds, listener, attributes); + search((LDAPConnection) ldap, baseDN, scope, filter, timeLimitSeconds, + ignoreReferralErrors, listener, attributes); } else if (ldap instanceof LDAPConnectionPool) { - search((LDAPConnectionPool) ldap, baseDN, scope, filter, timeLimitSeconds, listener, attributes); + search((LDAPConnectionPool) ldap, baseDN, scope, filter, timeLimitSeconds, + ignoreReferralErrors, listener, attributes); } else { throw new IllegalArgumentException("unsupported LDAPInterface implementation: " + ldap); } @@ -159,14 +190,23 @@ public final class LdapUtils { /** * This method performs an asynchronous ldap search operation that could have multiple results */ - public static void search(LDAPConnection ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener> listener, String... attributes) { - LdapSearchResultListener searchResultListener = new LdapSearchResultListener(ldap, - (asyncRequestID, searchResult) -> listener.onResponse(Collections.unmodifiableList(searchResult.getSearchEntries())), 1); - + public static void search(LDAPConnection ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener> listener, + String... attributes) { + LdapSearchResultListener searchResultListener = new LdapSearchResultListener( + ldap, + ignoreReferralErrors, + ActionListener.wrap( + searchResult -> listener.onResponse( + Collections.unmodifiableList(searchResult.getSearchEntries()) + ), + listener::onFailure), + 1); try { - SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, - false, filter, attributes); + SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, + DereferencePolicy.NEVER, 0, timeLimitSeconds, false, filter, attributes); searchResultListener.setSearchRequest(request); ldap.asyncSearch(request); } catch (LDAPException e) { @@ -177,20 +217,35 @@ public final class LdapUtils { /** * This method performs an asynchronous ldap search operation that could have multiple results */ - public static void search(LDAPConnectionPool ldap, String baseDN, SearchScope scope, Filter filter, int timeLimitSeconds, - ActionListener> listener, String... attributes) { + public static void search(LDAPConnectionPool ldap, String baseDN, SearchScope scope, + Filter filter, int timeLimitSeconds, + boolean ignoreReferralErrors, + ActionListener> listener, + String... attributes) { boolean searching = false; LDAPConnection ldapConnection = null; try { ldapConnection = ldap.getConnection(); final LDAPConnection finalConnection = ldapConnection; - LdapSearchResultListener ldapSearchResultListener = new LdapSearchResultListener(ldapConnection, - (asyncRequestID, searchResult) -> { - IOUtils.closeWhileHandlingException(() -> ldap.releaseConnection(finalConnection)); - listener.onResponse(Collections.unmodifiableList(searchResult.getSearchEntries())); - }, 1); - SearchRequest request = new SearchRequest(ldapSearchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, - false, filter, attributes); + LdapSearchResultListener ldapSearchResultListener = new LdapSearchResultListener( + ldapConnection, ignoreReferralErrors, + ActionListener.wrap( + searchResult -> { + IOUtils.closeWhileHandlingException( + () -> ldap.releaseConnection(finalConnection) + ); + listener.onResponse(Collections.unmodifiableList( + searchResult.getSearchEntries() + )); + }, (e) -> { + IOUtils.closeWhileHandlingException( + () -> ldap.releaseConnection(finalConnection) + ); + listener.onFailure(e); + }), + 1); + SearchRequest request = new SearchRequest(ldapSearchResultListener, baseDN, scope, + DereferencePolicy.NEVER, 0, timeLimitSeconds, false, filter, attributes); ldapSearchResultListener.setSearchRequest(request); finalConnection.asyncSearch(request); searching = true; @@ -204,9 +259,51 @@ public final class LdapUtils { } } - public static Filter createFilter(String filterTemplate, String... arguments) throws LDAPException { - return Filter.create(new MessageFormat(filterTemplate, Locale.ROOT).format((Object[]) encodeFilterValues(arguments), - new StringBuffer(), null).toString()); + /** + * Returns true if the provide {@link SearchResult} was successfully completed + * by the server. + * Note: Referrals are not considered a successful response for the + * purposes of this method. + */ + private static boolean isSuccess(SearchResult searchResult) { + switch (searchResult.getResultCode().intValue()) { + case ResultCode.SUCCESS_INT_VALUE: + case ResultCode.COMPARE_FALSE_INT_VALUE: + case ResultCode.COMPARE_TRUE_INT_VALUE: + return true; + default: + return false; + } + } + + private static SearchResult emptyResult(SearchResult parentResult) { + return new SearchResult( + parentResult.getMessageID(), + ResultCode.SUCCESS, + "Empty result", + parentResult.getMatchedDN(), + null, + 0, + 0, + null + ); + } + + private static LDAPException toException(SearchResult searchResult) { + return new LDAPException( + searchResult.getResultCode(), + searchResult.getDiagnosticMessage(), + searchResult.getMatchedDN(), + searchResult.getReferralURLs(), + searchResult.getResponseControls() + ); + } + + public static Filter createFilter(String filterTemplate, String... arguments) + throws LDAPException { + return Filter.create(new MessageFormat(filterTemplate, Locale.ROOT) + .format((Object[]) encodeFilterValues(arguments), new StringBuffer(), null) + .toString()); } public static String[] attributesToSearchFor(String[] attributes) { @@ -222,35 +319,41 @@ public final class LdapUtils { private static class SingleEntryListener extends LdapSearchResultListener { - SingleEntryListener(LDAPConnection ldapConnection, ActionListener listener, Filter filter) { - super(ldapConnection, ((asyncRequestID, searchResult) -> { - final List entryList = searchResult.getSearchEntries(); - if (entryList.size() > 1) { - listener.onFailure(Exceptions.authenticationError("multiple search results found for [{}]", filter)); - } else if (entryList.size() == 1) { - listener.onResponse(entryList.get(0)); - } else { - listener.onResponse(null); - } - }), 1); + SingleEntryListener(LDAPConnection ldapConnection, + ActionListener listener, Filter filter, + boolean ignoreReferralErrors) { + super(ldapConnection, ignoreReferralErrors, ActionListener.wrap(searchResult -> { + final List entryList = searchResult.getSearchEntries(); + if (entryList.size() > 1) { + listener.onFailure(Exceptions.authenticationError( + "multiple search results found for [{}]", filter)); + } else if (entryList.size() == 1) { + listener.onResponse(entryList.get(0)); + } else { + listener.onResponse(null); + } + }, listener::onFailure) + , 1); } - } private static class LdapSearchResultListener implements AsyncSearchResultListener { - private static final Logger LOGGER = ESLoggerFactory.getLogger(LdapUtils.class); private final List entryList = new ArrayList<>(); private final List referenceList = new ArrayList<>(); protected final SetOnce searchRequestRef = new SetOnce<>(); - private final BiConsumer consumer; + private final LDAPConnection ldapConnection; + private final boolean ignoreReferralErrors; + private final ActionListener listener; private final int depth; - LdapSearchResultListener(LDAPConnection ldapConnection, BiConsumer consumer, int depth) { + LdapSearchResultListener(LDAPConnection ldapConnection, boolean ignoreReferralErrors, + ActionListener listener, int depth) { this.ldapConnection = ldapConnection; - this.consumer = consumer; + this.listener = listener; this.depth = depth; + this.ignoreReferralErrors = ignoreReferralErrors; } @Override @@ -265,72 +368,97 @@ public final class LdapUtils { @Override public void searchResultReceived(AsyncRequestID requestID, SearchResult searchResult) { - // whenever we get a search result we need to check for a referral. A referral is a mechanism for an LDAP server to reference - // an object stored in a different LDAP server/partition. There are cases where we need to follow a referral in order to get - // the actual object we are searching for + // Whenever we get a search result we need to check for a referral. + // A referral is a mechanism for an LDAP server to reference an object stored in a + // different LDAP server/partition. There are cases where we need to follow a referral + // in order to get the actual object we are searching for final String[] referralUrls = referenceList.stream() .flatMap((ref) -> Arrays.stream(ref.getReferralURLs())) .collect(Collectors.toList()) .toArray(Strings.EMPTY_ARRAY); - final SearchRequest searchRequest = searchRequestRef.get(); - if (referralUrls.length == 0 || searchRequest.followReferrals(ldapConnection) == false) { - // either no referrals to follow or we have explicitly disabled referral following on the connection so we just create - // a new search result that has the values we've collected. The search result passed to this method will not have of the - // entries as we are using a result listener and the results are not being collected by the LDAP library - LOGGER.trace("LDAP Search {} => {} ({})", searchRequest, searchResult, entryList); - SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), searchResult.getResultCode(), searchResult - .getDiagnosticMessage(), searchResult.getMatchedDN(), referralUrls, entryList, referenceList, entryList.size(), - referenceList.size(), searchResult.getResponseControls()); - consumer.accept(requestID, resultWithValues); + final SearchRequest request = searchRequestRef.get(); + if (referralUrls.length == 0 || request.followReferrals(ldapConnection) == false) { + // either no referrals to follow or we have explicitly disabled referral following + // on the connection so we just create a new search result that has the values we've + // collected. The search result passed to this method will not have of the entries + // as we are using a result listener and the results are not being collected by the + // LDAP library + LOGGER.trace("LDAP Search {} => {} ({})", request, searchResult, entryList); + if (isSuccess(searchResult)) { + SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), + searchResult.getResultCode(), searchResult.getDiagnosticMessage(), + searchResult.getMatchedDN(), referralUrls, entryList, referenceList, + entryList.size(), referenceList.size(), + searchResult.getResponseControls()); + listener.onResponse(resultWithValues); + } else { + listener.onFailure(toException(searchResult)); + } } else if (depth >= ldapConnection.getConnectionOptions().getReferralHopLimit()) { - // we've gone through too many levels of referrals so we terminate with the values collected so far and the proper result - // code to indicate the search was terminated early - LOGGER.trace("Referral limit exceeded {} => {} ({})", searchRequest, searchResult, entryList); - SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), ResultCode.REFERRAL_LIMIT_EXCEEDED, - searchResult.getDiagnosticMessage(), searchResult.getMatchedDN(), referralUrls, entryList, referenceList, - entryList.size(), referenceList.size(), searchResult.getResponseControls()); - consumer.accept(requestID, resultWithValues); + // we've gone through too many levels of referrals so we terminate with the values + // collected so far and the proper result code to indicate the search was + // terminated early + LOGGER.trace("Referral limit exceeded {} => {} ({})", + request, searchResult, entryList); + listener.onFailure(new LDAPException(ResultCode.REFERRAL_LIMIT_EXCEEDED, + "Referral limit exceeded (" + depth + ")", + searchResult.getMatchedDN(), referralUrls, + searchResult.getResponseControls())); } else { if (LOGGER.isTraceEnabled()) { - LOGGER.trace("LDAP referred elsewhere {} => {}", searchRequest, Arrays.toString(referralUrls)); + LOGGER.trace("LDAP referred elsewhere {} => {}", + request, Arrays.toString(referralUrls)); } // there are referrals to follow, so we start the process to follow the referrals final CountDown countDown = new CountDown(referralUrls.length); final List referralUrlsList = new ArrayList<>(Arrays.asList(referralUrls)); - BiConsumer referralConsumer = (reqID, innerResult) -> { - // synchronize here since we are possibly sending out a lot of requests and the result lists are not thread safe and - // this also provides us with a consistent view - synchronized (this) { - if (innerResult.getSearchEntries() != null) { - entryList.addAll(innerResult.getSearchEntries()); - } - if (innerResult.getSearchReferences() != null) { - referenceList.addAll(innerResult.getSearchReferences()); - } - } + ActionListener referralListener = ActionListener.wrap( + innerResult -> { + // synchronize here since we are possibly sending out a lot of requests + // and the result lists are not thread safe and this also provides us + // with a consistent view + synchronized (this) { + if (innerResult.getSearchEntries() != null) { + entryList.addAll(innerResult.getSearchEntries()); + } + if (innerResult.getSearchReferences() != null) { + referenceList.addAll(innerResult.getSearchReferences()); + } + } - // count down and once all referrals have been traversed then we can create the results - if (countDown.countDown()) { - SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), searchResult.getResultCode(), - searchResult.getDiagnosticMessage(), searchResult.getMatchedDN(), - referralUrlsList.toArray(Strings.EMPTY_ARRAY), entryList, referenceList, - entryList.size(), referenceList.size(), searchResult.getResponseControls()); - consumer.accept(requestID, resultWithValues); - } - }; + // count down and once all referrals have been traversed then we can + // create the results + if (countDown.countDown()) { + SearchResult resultWithValues = new SearchResult( + searchResult.getMessageID(), searchResult.getResultCode(), + searchResult.getDiagnosticMessage(), + searchResult.getMatchedDN(), + referralUrlsList.toArray(Strings.EMPTY_ARRAY), entryList, + referenceList, entryList.size(), referenceList.size(), + searchResult.getResponseControls()); + listener.onResponse(resultWithValues); + } + }, listener::onFailure); for (String referralUrl : referralUrls) { try { - // for each referral follow it and any other referrals returned until we get to a depth that is greater than or - // equal to the referral hop limit or all referrals have been followed. Each time referrals are followed from a - // search result, the depth increases by 1 - followReferral(ldapConnection, referralUrl, searchRequest, referralConsumer, depth + 1, searchResult, requestID); + // for each referral follow it and any other referrals returned until we + // get to a depth that is greater than or equal to the referral hop limit + // or all referrals have been followed. Each time referrals are followed + // from a search result, the depth increases by 1 + followReferral(ldapConnection, referralUrl, request, referralListener, + depth + 1, ignoreReferralErrors, searchResult); } catch (LDAPException e) { - LOGGER.warn((Supplier) - () -> new ParameterizedMessage("caught exception while trying to follow referral [{}]", referralUrl), e); - referralConsumer.accept(requestID, new SearchResult(searchResult.getMessageID(), e.getResultCode(), - e.getDiagnosticMessage(), e.getMatchedDN(), e.getReferralURLs(), 0, 0, e.getResponseControls())); + LOGGER.warn((Supplier) () -> new ParameterizedMessage( + "caught exception while trying to follow referral [{}]", + referralUrl), e); + if (ignoreReferralErrors) { + // Needed in order for the countDown to be correct + referralListener.onResponse(emptyResult(searchResult)); + } else { + listener.onFailure(e); + } } } } @@ -342,68 +470,88 @@ public final class LdapUtils { } /** - * Performs the actual connection and following of a referral given a URL string. This referral is being followed as it may contain a - * result that is relevant to our search + * Performs the actual connection and following of a referral given a URL string. + * This referral is being followed as it may contain a result that is relevant to our search */ - private static void followReferral(LDAPConnection ldapConnection, String urlString, SearchRequest searchRequest, - BiConsumer consumer, int depth, - SearchResult originatingResult, AsyncRequestID asyncRequestID) throws LDAPException { + private static void followReferral(LDAPConnection ldapConnection, String urlString, + SearchRequest searchRequest, + ActionListener listener, int depth, + boolean ignoreErrors, SearchResult originatingResult) + throws LDAPException { + final LDAPURL referralURL = new LDAPURL(urlString); final String host = referralURL.getHost(); - // the host must be present in order to follow a referral - if (host != null) { - // the referral URL often contains information necessary about the LDAP request such as the base DN, scope, and filter. If it - // does not, then we reuse the values from the originating search request - final String requestBaseDN; - if (referralURL.baseDNProvided()) { - requestBaseDN = referralURL.getBaseDN().toString(); - } else { - requestBaseDN = searchRequest.getBaseDN(); - } + if (host == null) { + // nothing to really do since a null host cannot really be handled, so we treat it as + // an error + throw new LDAPException(ResultCode.UNAVAILABLE, "Null referral host in " + urlString); + } - final SearchScope requestScope; - if (referralURL.scopeProvided()) { - requestScope = referralURL.getScope(); - } else { - requestScope = searchRequest.getScope(); - } - - final Filter requestFilter; - if (referralURL.filterProvided()) { - requestFilter = referralURL.getFilter(); - } else { - requestFilter = searchRequest.getFilter(); - } - - // in order to follow the referral we need to open a new connection and we do so using the referral connector on the ldap - // connection - final LDAPConnection referralConn = - ldapConnection.getReferralConnector().getReferralConnection(referralURL, ldapConnection); - final LdapSearchResultListener listener = new LdapSearchResultListener(referralConn, - (reqId, searchResult) -> { - IOUtils.closeWhileHandlingException(referralConn); - consumer.accept(reqId, searchResult); - }, depth); - boolean success = false; - try { - final SearchRequest referralSearchRequest = - new SearchRequest(listener, searchRequest.getControls(), - requestBaseDN, requestScope, searchRequest.getDereferencePolicy(), - searchRequest.getSizeLimit(), searchRequest.getTimeLimitSeconds(), searchRequest.typesOnly(), - requestFilter, searchRequest.getAttributes()); - listener.setSearchRequest(searchRequest); - referralConn.asyncSearch(referralSearchRequest); - success = true; - } finally { - if (success == false) { - IOUtils.closeWhileHandlingException(referralConn); - } - } + // the referral URL often contains information necessary about the LDAP request such as + // the base DN, scope, and filter. If it does not, then we reuse the values from the + // originating search request + final String requestBaseDN; + if (referralURL.baseDNProvided()) { + requestBaseDN = referralURL.getBaseDN().toString(); } else { - // nothing to really do since a null host cannot really be handled, so we just return with a response that is empty... - consumer.accept(asyncRequestID, new SearchResult(originatingResult.getMessageID(), ResultCode.UNAVAILABLE, - null, null, null, Collections.emptyList(), Collections.emptyList(), 0, 0, null)); + requestBaseDN = searchRequest.getBaseDN(); + } + + final SearchScope requestScope; + if (referralURL.scopeProvided()) { + requestScope = referralURL.getScope(); + } else { + requestScope = searchRequest.getScope(); + } + + final Filter requestFilter; + if (referralURL.filterProvided()) { + requestFilter = referralURL.getFilter(); + } else { + requestFilter = searchRequest.getFilter(); + } + + // in order to follow the referral we need to open a new connection and we do so using the + // referral connector on the ldap connection + final LDAPConnection referralConn = ldapConnection.getReferralConnector() + .getReferralConnection(referralURL, ldapConnection); + final LdapSearchResultListener ldapListener = new LdapSearchResultListener( + referralConn, ignoreErrors, + ActionListener.wrap( + searchResult -> { + IOUtils.closeWhileHandlingException(referralConn); + listener.onResponse(searchResult); + }, + e -> { + if (ignoreErrors) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(new ParameterizedMessage( + "Failed to retrieve results from referral URL [{}]." + + " Treating as 'no results'", + referralURL), e); + } + listener.onResponse(emptyResult(originatingResult)); + } else { + listener.onFailure(e); + } + }), + depth); + boolean success = false; + try { + final SearchRequest referralSearchRequest = + new SearchRequest(ldapListener, searchRequest.getControls(), + requestBaseDN, requestScope, searchRequest.getDereferencePolicy(), + searchRequest.getSizeLimit(), searchRequest.getTimeLimitSeconds(), + searchRequest.typesOnly(), requestFilter, + searchRequest.getAttributes()); + ldapListener.setSearchRequest(searchRequest); + referralConn.asyncSearch(referralSearchRequest); + success = true; + } finally { + if (success == false) { + IOUtils.closeWhileHandlingException(referralConn); + } } } } diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactory.java index 4360063499d..54c35d2863d 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactory.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/SessionFactory.java @@ -34,7 +34,8 @@ import java.util.regex.Pattern; /** * This factory holds settings needed for authenticating to LDAP and creating LdapConnections. - * Each created LdapConnection needs to be closed or else connections will pill up consuming resources. + * Each created LdapConnection needs to be closed or else connections will pill up consuming + * resources. *

* A standard looking usage pattern could look like this: *

@@ -52,9 +53,14 @@ public abstract class SessionFactory {
     public static final String TIMEOUT_LDAP_SETTING = "timeout.ldap_search";
     public static final String HOSTNAME_VERIFICATION_SETTING = "hostname_verification";
     public static final String FOLLOW_REFERRALS_SETTING = "follow_referrals";
+    public static final Setting IGNORE_REFERRAL_ERRORS_SETTING = Setting.boolSetting(
+            "ignore_referral_errors", true, Setting.Property.NodeScope);
+
     public static final TimeValue TIMEOUT_DEFAULT = TimeValue.timeValueSeconds(5);
-    private static final Pattern STARTS_WITH_LDAPS = Pattern.compile("^ldaps:.*", Pattern.CASE_INSENSITIVE);
-    private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*", Pattern.CASE_INSENSITIVE);
+    private static final Pattern STARTS_WITH_LDAPS = Pattern.compile("^ldaps:.*",
+            Pattern.CASE_INSENSITIVE);
+    private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*",
+            Pattern.CASE_INSENSITIVE);
 
     protected final Logger logger;
     protected final RealmConfig config;
@@ -63,36 +69,43 @@ public abstract class SessionFactory {
 
     protected final ServerSet serverSet;
     protected final boolean sslUsed;
+    protected final boolean ignoreReferralErrors;
 
     protected SessionFactory(RealmConfig config, SSLService sslService) {
         this.config = config;
         this.logger = config.logger(getClass());
-        TimeValue searchTimeout = config.settings().getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
+        final Settings settings = config.settings();
+        TimeValue searchTimeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
         if (searchTimeout.millis() < 1000L) {
-            logger.warn("ldap_search timeout [{}] is less than the minimum supported search timeout of 1s. using 1s",
+            logger.warn("ldap_search timeout [{}] is less than the minimum supported search " +
+                            "timeout of 1s. using 1s",
                     searchTimeout.millis());
             searchTimeout = TimeValue.timeValueSeconds(1L);
         }
         this.timeout = searchTimeout;
         this.sslService = sslService;
-        LDAPServers ldapServers = ldapServers(config.settings());
+        LDAPServers ldapServers = ldapServers(settings);
         this.serverSet = serverSet(config, sslService, ldapServers);
         this.sslUsed = ldapServers.ssl;
+        this.ignoreReferralErrors = IGNORE_REFERRAL_ERRORS_SETTING.get(settings);
     }
 
     /**
-     * Authenticates the given user and opens a new connection that bound to it (meaning, all operations
-     * under the returned connection will be executed on behalf of the authenticated user.
+     * Authenticates the given user and opens a new connection that bound to it (meaning, all
+     * operations under the returned connection will be executed on behalf of the authenticated
+     * user.
      *
      * @param user     The name of the user to authenticate the connection with.
      * @param password The password of the user
      * @param listener the listener to call on a failure or result
      */
-    public abstract void session(String user, SecuredString password, ActionListener listener);
+    public abstract void session(String user, SecuredString password,
+                                 ActionListener listener);
 
     /**
-     * Returns a flag to indicate if this session factory supports unauthenticated sessions. This means that a session can
-     * be established without providing any credentials in a call to {@link #unauthenticatedSession(String, ActionListener)}
+     * Returns a flag to indicate if this session factory supports unauthenticated sessions.
+     * This means that a session can be established without providing any credentials in a call to
+     * {@link #unauthenticatedSession(String, ActionListener)}
      *
      * @return true if the factory supports unauthenticated sessions
      */
@@ -110,29 +123,43 @@ public abstract class SessionFactory {
         throw new UnsupportedOperationException("unauthenticated sessions are not supported");
     }
 
-    protected static LDAPConnectionOptions connectionOptions(RealmConfig config, SSLService sslService, Logger logger) {
+    protected static LDAPConnectionOptions connectionOptions(RealmConfig config,
+                                                             SSLService sslService, Logger logger) {
         Settings realmSettings = config.settings();
         LDAPConnectionOptions options = new LDAPConnectionOptions();
-        options.setConnectTimeoutMillis(Math.toIntExact(realmSettings.getAsTime(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT).millis()));
+        options.setConnectTimeoutMillis(Math.toIntExact(
+                realmSettings.getAsTime(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT).millis()
+        ));
         options.setFollowReferrals(realmSettings.getAsBoolean(FOLLOW_REFERRALS_SETTING, true));
-        options.setResponseTimeoutMillis(realmSettings.getAsTime(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT).millis());
+        options.setResponseTimeoutMillis(
+                realmSettings.getAsTime(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT).millis()
+        );
         options.setAllowConcurrentSocketFactoryUse(true);
-        SSLConfigurationSettings sslConfigurationSettings = SSLConfigurationSettings.withoutPrefix();
+
+        final SSLConfigurationSettings sslConfigurationSettings =
+                SSLConfigurationSettings.withoutPrefix();
         final Settings realmSSLSettings = realmSettings.getByPrefix("ssl.");
-        final boolean verificationModeExists = sslConfigurationSettings.verificationMode.exists(realmSSLSettings);
-        final boolean hostnameVerficationExists = realmSettings.get(HOSTNAME_VERIFICATION_SETTING, null) != null;
+        final boolean verificationModeExists =
+                sslConfigurationSettings.verificationMode.exists(realmSSLSettings);
+        final boolean hostnameVerficationExists =
+                realmSettings.get(HOSTNAME_VERIFICATION_SETTING, null) != null;
+
         if (verificationModeExists && hostnameVerficationExists) {
             throw new IllegalArgumentException("[" + HOSTNAME_VERIFICATION_SETTING + "] and [" +
-                    sslConfigurationSettings.verificationMode.getKey() + "] may not be used at the same time");
+                    sslConfigurationSettings.verificationMode.getKey() +
+                    "] may not be used at the same time");
         } else if (verificationModeExists) {
-            VerificationMode verificationMode = sslService.getVerificationMode(realmSSLSettings, Settings.EMPTY);
+            VerificationMode verificationMode = sslService.getVerificationMode(realmSSLSettings,
+                    Settings.EMPTY);
             if (verificationMode == VerificationMode.FULL) {
                 options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true));
             }
         } else if (hostnameVerficationExists) {
-            new DeprecationLogger(logger).deprecated("the setting [{}] has been deprecated and will be removed in a future version. use " +
-                            "[{}] instead", RealmSettings.getFullSettingKey(config, HOSTNAME_VERIFICATION_SETTING),
-                    RealmSettings.getFullSettingKey(config, "ssl." + sslConfigurationSettings.verificationMode.getKey()));
+            new DeprecationLogger(logger).deprecated("the setting [{}] has been deprecated and " +
+                            "will be removed in a future version. use [{}] instead",
+                    RealmSettings.getFullSettingKey(config, HOSTNAME_VERIFICATION_SETTING),
+                    RealmSettings.getFullSettingKey(config, "ssl." +
+                            sslConfigurationSettings.verificationMode.getKey()));
             if (realmSettings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
                 options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true));
             }
@@ -146,7 +173,8 @@ public abstract class SessionFactory {
         // Parse LDAP urls
         String[] ldapUrls = settings.getAsArray(URLS_SETTING, getDefaultLdapUrls(settings));
         if (ldapUrls == null || ldapUrls.length == 0) {
-            throw new IllegalArgumentException("missing required LDAP setting [" + URLS_SETTING + "]");
+            throw new IllegalArgumentException("missing required LDAP setting [" + URLS_SETTING +
+                    "]");
         }
         return new LDAPServers(ldapUrls);
     }
@@ -155,7 +183,8 @@ public abstract class SessionFactory {
         return null;
     }
 
-    private ServerSet serverSet(RealmConfig realmConfig, SSLService clientSSLService, LDAPServers ldapServers) {
+    private ServerSet serverSet(RealmConfig realmConfig, SSLService clientSSLService,
+                                LDAPServers ldapServers) {
         Settings settings = realmConfig.settings();
         SocketFactory socketFactory = null;
         if (ldapServers.ssl()) {
@@ -166,8 +195,8 @@ public abstract class SessionFactory {
                 logger.debug("using encryption for LDAP connections without hostname verification");
             }
         }
-        return LdapLoadBalancing.serverSet(ldapServers.addresses(), ldapServers.ports(), settings, socketFactory,
-                connectionOptions(realmConfig, sslService, logger));
+        return LdapLoadBalancing.serverSet(ldapServers.addresses(), ldapServers.ports(), settings,
+                socketFactory, connectionOptions(realmConfig, sslService, logger));
     }
 
     // package private to use for testing
@@ -182,12 +211,19 @@ public abstract class SessionFactory {
     protected static Set> getSettings() {
         Set> settings = new HashSet<>();
         settings.addAll(LdapLoadBalancing.getSettings());
-        settings.add(Setting.listSetting(URLS_SETTING, Collections.emptyList(), Function.identity(), Setting.Property.NodeScope));
-        settings.add(Setting.timeSetting(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope));
-        settings.add(Setting.timeSetting(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope));
-        settings.add(Setting.timeSetting(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope));
-        settings.add(Setting.boolSetting(HOSTNAME_VERIFICATION_SETTING, true, Setting.Property.NodeScope));
-        settings.add(Setting.boolSetting(FOLLOW_REFERRALS_SETTING, true, Setting.Property.NodeScope));
+        settings.add(Setting.listSetting(URLS_SETTING, Collections.emptyList(), Function.identity(),
+                Setting.Property.NodeScope));
+        settings.add(Setting.timeSetting(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT,
+                Setting.Property.NodeScope));
+        settings.add(Setting.timeSetting(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT,
+                Setting.Property.NodeScope));
+        settings.add(Setting.timeSetting(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT,
+                Setting.Property.NodeScope));
+        settings.add(Setting.boolSetting(HOSTNAME_VERIFICATION_SETTING, true,
+                Setting.Property.NodeScope));
+        settings.add(Setting.boolSetting(FOLLOW_REFERRALS_SETTING, true,
+                Setting.Property.NodeScope));
+        settings.add(IGNORE_REFERRAL_ERRORS_SETTING);
         settings.addAll(SSLConfigurationSettings.withPrefix("ssl.").getAllSettings());
         return settings;
     }
@@ -208,7 +244,8 @@ public abstract class SessionFactory {
                     addresses[i] = url.getHost();
                     ports[i] = url.getPort();
                 } catch (LDAPException e) {
-                    throw new IllegalArgumentException("unable to parse configured LDAP url [" + urls[i] + "]", e);
+                    throw new IllegalArgumentException("unable to parse configured LDAP url [" +
+                            urls[i] + "]", e);
                 }
             }
         }
@@ -233,13 +270,16 @@ public abstract class SessionFactory {
                 return true;
             }
 
-            final boolean allSecure = Arrays.stream(ldapUrls).allMatch(s -> STARTS_WITH_LDAPS.matcher(s).find());
-            final boolean allClear = Arrays.stream(ldapUrls).allMatch(s -> STARTS_WITH_LDAP.matcher(s).find());
+            final boolean allSecure = Arrays.stream(ldapUrls)
+                    .allMatch(s -> STARTS_WITH_LDAPS.matcher(s).find());
+            final boolean allClear = Arrays.stream(ldapUrls)
+                    .allMatch(s -> STARTS_WITH_LDAP.matcher(s).find());
 
             if (!allSecure && !allClear) {
                 //No mixing is allowed because we use the same socketfactory
-                throw new IllegalArgumentException("configured LDAP protocols are not all equal (ldaps://.. and ldap://..): [" +
-                        Strings.arrayToCommaDelimitedString(ldapUrls) + "]");
+                throw new IllegalArgumentException(
+                        "configured LDAP protocols are not all equal (ldaps://.. and ldap://..): ["
+                                +  Strings.arrayToCommaDelimitedString(ldapUrls) + "]");
             }
 
             return allSecure;
diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java
index 61fe9dcba26..256d474fdc9 100644
--- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java
+++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java
@@ -24,15 +24,17 @@ import static org.hamcrest.Matchers.is;
 @Network
 public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
 
-    private static final String BRUCE_BANNER_DN = "cn=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
+    private static final String BRUCE_BANNER_DN =
+            "cn=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
 
     public void testResolveSubTree() throws Exception {
         Settings settings = Settings.builder()
                 .put("scope", LdapSearchScope.SUB_TREE)
                 .build();
-        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
-        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10),
-                NoOpLogger.INSTANCE, null);
+        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
+                "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
+        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
+                TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
         assertThat(groups, containsInAnyOrder(
                 containsString("Avengers"),
                 containsString("SHIELD"),
@@ -48,9 +50,10 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
                 .put("scope", LdapSearchScope.ONE_LEVEL)
                 .put("base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com")
                 .build();
-        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
-        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10),
-                NoOpLogger.INSTANCE, null);
+        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
+                "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
+        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
+                TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
         assertThat(groups, hasItem(containsString("Users")));
     }
 
@@ -59,9 +62,10 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
                 .put("scope", LdapSearchScope.BASE)
                 .put("base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com")
                 .build();
-        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
-        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10),
-                NoOpLogger.INSTANCE, null);
+        ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
+                "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
+        List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
+                TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
         assertThat(groups, hasItem(containsString("CN=Users,CN=Builtin")));
     }
 
@@ -74,7 +78,8 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
             };
             final String dn = "CN=Jarvis, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com";
             PlainActionFuture future = new PlainActionFuture<>();
-            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn, TimeValue.timeValueSeconds(10), future);
+            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn,
+                    TimeValue.timeValueSeconds(10), false, future);
             Filter query = future.actionGet();
             assertValidSidQuery(query, expectedSids);
         }
@@ -87,7 +92,8 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
                     "S-1-5-21-3510024162-210737641-214529065-1117"}; //Gods group
             final String dn = "CN=Odin, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com";
             PlainActionFuture future = new PlainActionFuture<>();
-            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn, TimeValue.timeValueSeconds(10), future);
+            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn,
+                    TimeValue.timeValueSeconds(10), false, future);
             Filter query = future.actionGet();
             assertValidSidQuery(query, expectedSids);
         }
@@ -105,7 +111,8 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
 
             final String dn = BRUCE_BANNER_DN;
             PlainActionFuture future = new PlainActionFuture<>();
-            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn, TimeValue.timeValueSeconds(10), future);
+            ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, dn,
+                    TimeValue.timeValueSeconds(10), false, future);
             Filter query = future.actionGet();
             assertValidSidQuery(query, expectedSids);
         }
diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java
index fc7bcf3058e..42756c7e84e 100644
--- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java
+++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java
@@ -6,6 +6,7 @@
 package org.elasticsearch.xpack.security.authc.ldap;
 
 import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.ResultCode;
 import org.elasticsearch.action.support.PlainActionFuture;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
@@ -32,6 +33,8 @@ import static org.hamcrest.Matchers.is;
 @Network
 public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryIntegTests {
 
+    private final SecuredString SECURED_PASSWORD = SecuredStringTests.build(PASSWORD);
+
     @Override
     public boolean enableWarningsCheck() {
         return false;
@@ -39,11 +42,14 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
     @SuppressWarnings("unchecked")
     public void testAdAuth() throws Exception {
-        RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false), globalSettings);
-        ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
+        RealmConfig config = new RealmConfig("ad-test",
+                buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false),
+                globalSettings);
+        ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config,
+                sslService);
 
         String userName = "ironman";
-        try (LdapSession ldap = session(sessionFactory, userName, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
             List groups = groups(ldap);
             assertThat(groups, containsInAnyOrder(
                     containsString("Geniuses"),
@@ -64,7 +70,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         String userName = "ades\\ironman";
-        try (LdapSession ldap = session(sessionFactory, userName, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
             List groups = groups(ldap);
             assertThat(groups, containsInAnyOrder(
                     containsString("Geniuses"),
@@ -91,7 +97,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         PlainActionFuture> groups = new PlainActionFuture<>();
-        session(sessionFactory, "ironman", SecuredStringTests.build(PASSWORD)).groups(groups);
+        session(sessionFactory, "ironman", SECURED_PASSWORD).groups(groups);
         LDAPException expected = expectThrows(LDAPException.class, groups::actionGet);
         assertThat(expected.getMessage(), containsString("A client-side timeout was encountered while waiting"));
     }
@@ -102,7 +108,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
         String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", };
         for(String user: users) {
-            try (LdapSession ldap = session(sessionFactory, user, SecuredStringTests.build(PASSWORD))) {
+            try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) {
                 assertThat("group avenger test for user "+user, groups(ldap), hasItem(containsString("Avengers")));
             }
         }
@@ -116,7 +122,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         String userName = "hulk";
-        try (LdapSession ldap = session(sessionFactory, userName, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
             List groups = groups(ldap);
 
             assertThat(groups, containsInAnyOrder(
@@ -138,7 +144,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         String userName = "hulk";
-        try (LdapSession ldap = session(sessionFactory, userName, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
             List groups = groups(ldap);
 
             assertThat(groups, containsInAnyOrder(
@@ -164,7 +170,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         String userName = "hulk";
-        try (LdapSession ldap = session(sessionFactory, userName, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
             List groups = groups(ldap);
 
             assertThat(groups, hasItem(containsString("Avengers")));
@@ -180,7 +186,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
         //Login with the UserPrincipalName
         String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
-        try (LdapSession ldap = session(sessionFactory, "erik.selvig", SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) {
             List groups = groups(ldap);
             assertThat(ldap.userDn(), is(userDN));
             assertThat(groups, containsInAnyOrder(
@@ -198,7 +204,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
         //login with sAMAccountName
         String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
-        try (LdapSession ldap = session(sessionFactory, "selvig", SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, "selvig", SECURED_PASSWORD)) {
             assertThat(ldap.userDn(), is(userDN));
 
             List groups = groups(ldap);
@@ -221,7 +227,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
 
         //Login with the UserPrincipalName
-        try (LdapSession ldap = session(sessionFactory, "erik.selvig", SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) {
             List groups = groups(ldap);
             assertThat(groups, containsInAnyOrder(
                     containsString("CN=Geniuses"),
@@ -235,7 +241,13 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
     public void testStandardLdapConnection() throws Exception {
         String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
         String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
-        Settings settings = LdapTestCase.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE);
+        Settings settings = LdapTestCase.buildLdapSettings(
+                new String[] { AD_LDAP_URL },
+                new String[] { userTemplate },
+                groupSearchBase,
+                LdapSearchScope.SUB_TREE,
+                null,
+                true);
         if (useGlobalSSL == false) {
             settings = Settings.builder()
                     .put(settings)
@@ -247,7 +259,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService);
 
         String user = "Bruce Banner";
-        try (LdapSession ldap = session(sessionFactory, user, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) {
             List groups = groups(ldap);
 
             assertThat(groups, containsInAnyOrder(
@@ -258,6 +270,42 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         }
     }
 
+    @SuppressWarnings("unchecked")
+    public void testHandlingLdapReferralErrors() throws Exception {
+        String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
+        String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
+        final boolean ignoreReferralErrors = false;
+        Settings settings = LdapTestCase.buildLdapSettings(
+                new String[] { AD_LDAP_URL },
+                new String[] { userTemplate },
+                groupSearchBase,
+                LdapSearchScope.SUB_TREE,
+                null,
+                ignoreReferralErrors);
+        if (useGlobalSSL == false) {
+            settings = Settings.builder()
+                    .put(settings)
+                    .put("ssl.truststore.path", getDataPath("../ldap/support/ldaptrust.jks"))
+                    .put("ssl.truststore.password", "changeit")
+                    .build();
+        }
+        RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings);
+        LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService);
+
+        String user = "Bruce Banner";
+        try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) {
+            final UncategorizedExecutionException exception = expectThrows(
+                    UncategorizedExecutionException.class,
+                    () -> groups(ldap)
+            );
+            final Throwable cause = exception.getCause();
+            assertThat(cause, instanceOf(ExecutionException.class));
+            assertThat(cause.getCause(), instanceOf(LDAPException.class));
+            final LDAPException ldapException = (LDAPException) cause.getCause();
+            assertThat(ldapException.getResultCode(), is(ResultCode.INVALID_CREDENTIALS));
+        }
+    }
+
     @SuppressWarnings("unchecked")
     public void testStandardLdapWithAttributeGroups() throws Exception {
         String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
@@ -273,7 +321,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
         LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService);
 
         String user = "Bruce Banner";
-        try (LdapSession ldap = session(sessionFactory, user, SecuredStringTests.build(PASSWORD))) {
+        try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) {
             List groups = groups(ldap);
 
             assertThat(groups, containsInAnyOrder(
@@ -290,7 +338,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
         String userName = "ironman";
         UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class,
-                () -> session(sessionFactory, userName, SecuredStringTests.build(PASSWORD)));
+                () -> session(sessionFactory, userName, SECURED_PASSWORD));
         assertThat(e.getCause(), instanceOf(ExecutionException.class));
         assertThat(e.getCause().getCause(), instanceOf(LDAPException.class));
         final LDAPException expected = (LDAPException) e.getCause().getCause();
@@ -309,7 +357,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
 
         String user = "Bruce Banner";
         UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class,
-                () -> session(sessionFactory, user, SecuredStringTests.build(PASSWORD)));
+                () -> session(sessionFactory, user, SECURED_PASSWORD));
         assertThat(e.getCause(), instanceOf(ExecutionException.class));
         assertThat(e.getCause().getCause(), instanceOf(LDAPException.class));
         final LDAPException expected = (LDAPException) e.getCause().getCause();
diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java
index 624f1b9f2bd..e750374ec46 100644
--- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java
+++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java
@@ -21,6 +21,7 @@ import org.elasticsearch.xpack.security.authc.RealmConfig;
 import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope;
 import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession;
 import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase;
+import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory;
 import org.elasticsearch.xpack.security.authc.support.SecuredString;
 import org.elasticsearch.xpack.security.authc.support.SecuredStringTests;
 import org.elasticsearch.xpack.ssl.SSLService;
@@ -307,8 +308,10 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase {
         String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
         String userSearchBase = "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
         Settings settings = Settings.builder()
-                .put(LdapTestCase.buildLdapSettings(new String[] { ActiveDirectorySessionFactoryTests.AD_LDAP_URL }, Strings.EMPTY_ARRAY,
-                        groupSearchBase, LdapSearchScope.SUB_TREE))
+                .put(LdapTestCase.buildLdapSettings(
+                        new String[] { ActiveDirectorySessionFactoryTests.AD_LDAP_URL },
+                        Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE, null,
+                        true))
                 .put("user_search.base_dn", userSearchBase)
                 .put("bind_dn", "ironman@ad.test.elasticsearch.com")
                 .put("bind_password", ActiveDirectorySessionFactoryTests.PASSWORD)
diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolverInMemoryTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolverInMemoryTests.java
new file mode 100644
index 00000000000..b237bbd985f
--- /dev/null
+++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/SearchGroupsResolverInMemoryTests.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+package org.elasticsearch.xpack.security.authc.ldap;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPConnectionOptions;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPURL;
+import com.unboundid.ldap.sdk.ResultCode;
+import org.elasticsearch.action.support.PlainActionFuture;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope;
+import org.elasticsearch.xpack.security.authc.ldap.support.LdapTestCase;
+import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils;
+
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+
+public class SearchGroupsResolverInMemoryTests extends LdapTestCase {
+
+    /**
+     * Tests that a client-side timeout in the asynchronous LDAP SDK is treated as a failure, rather
+     * than simply returning no results.
+     */
+    public void testSearchTimeoutIsFailure() throws Exception {
+
+        ldapServers[0].setProcessingDelayMillis(100);
+
+        final LDAPConnectionOptions options = new LDAPConnectionOptions();
+        options.setConnectTimeoutMillis(500);
+        options.setResponseTimeoutMillis(5);
+
+        final LDAPURL ldapurl = new LDAPURL(ldapUrls()[0]);
+        final LDAPConnection connection = LdapUtils.privilegedConnect(
+                () -> new LDAPConnection(options,
+                        ldapurl.getHost(), ldapurl.getPort())
+        );
+
+        final Settings settings = Settings.builder()
+                .put("group_search.base_dn", "ou=groups,o=sevenSeas")
+                .put("group_search.scope", LdapSearchScope.SUB_TREE)
+                .build();
+        final SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
+        final PlainActionFuture> future = new PlainActionFuture<>();
+        resolver.resolve(connection,
+                "cn=William Bush,ou=people,o=sevenSeas",
+                TimeValue.timeValueSeconds(30),
+                logger,
+                null, future);
+
+        final ExecutionException exception = expectThrows(ExecutionException.class, future::get);
+        final Throwable cause = exception.getCause();
+        assertThat(cause, instanceOf(LDAPException.class));
+        assertThat(((LDAPException) cause).getResultCode(), is(ResultCode.TIMEOUT));
+    }
+
+}
\ No newline at end of file
diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java
index e19d491808c..d98185af36d 100644
--- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java
+++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapTestCase.java
@@ -25,7 +25,6 @@ import org.junit.Before;
 import org.junit.BeforeClass;
 
 import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
 import java.util.List;
@@ -92,17 +91,27 @@ public abstract class LdapTestCase extends ESTestCase {
         return buildLdapSettings(ldapUrl, userTemplate, groupSearchBase, scope, null);
     }
 
-    public static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate, String groupSearchBase, LdapSearchScope scope,
+    public static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate,
+                                             String groupSearchBase, LdapSearchScope scope,
                                              LdapLoadBalancing serverSetType) {
+        return buildLdapSettings(ldapUrl, userTemplate, groupSearchBase, scope,
+                serverSetType, false);
+    }
+
+    public static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate,
+                                             String groupSearchBase, LdapSearchScope scope,
+                                             LdapLoadBalancing serverSetType,
+                                             boolean ignoreReferralErrors) {
         Settings.Builder builder = Settings.builder()
                 .putArray(URLS_SETTING, ldapUrl)
                 .putArray(USER_DN_TEMPLATES_SETTING_KEY, userTemplate)
+                .put(SessionFactory.IGNORE_REFERRAL_ERRORS_SETTING.getKey(), ignoreReferralErrors)
                 .put("group_search.base_dn", groupSearchBase)
                 .put("group_search.scope", scope)
                 .put("ssl.verification_mode", VerificationMode.CERTIFICATE);
         if (serverSetType != null) {
-            builder.put(LdapLoadBalancing.LOAD_BALANCE_SETTINGS + "." + LdapLoadBalancing.LOAD_BALANCE_TYPE_SETTING,
-                    serverSetType.toString());
+            builder.put(LdapLoadBalancing.LOAD_BALANCE_SETTINGS + "." +
+                            LdapLoadBalancing.LOAD_BALANCE_TYPE_SETTING, serverSetType.toString());
         }
         return builder.build();
     }