Failed LDAP SearchResults should be Exceptions (elastic/x-pack-elasticsearch#773)

When the LDAP SDK returns a SearchResult that has a non-success ResultCode, convert it to an exception and call onFailure

A configuration setting controls whether failures in referrals should be fatal (defaults to ignoring errors)

Closes: elastic/x-pack-elasticsearch#717

Original commit: elastic/x-pack-elasticsearch@4159758c2a
This commit is contained in:
Tim Vernum 2017-04-05 18:40:17 +10:00 committed by GitHub
parent 7c45cb7ccf
commit 7f0fd9e1a3
13 changed files with 673 additions and 307 deletions

View File

@ -475,12 +475,9 @@
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]LdapRealm.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]LdapSessionFactory.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]LdapUserSearchSessionFactory.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]SearchGroupsResolver.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]UserAttributeGroupsResolver.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]support[/\\]LdapLoadBalancing.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]support[/\\]LdapSession.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]support[/\\]LdapUtils.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]ldap[/\\]support[/\\]SessionFactory.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]pki[/\\]PkiRealm.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]support[/\\]BCrypt.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]support[/\\]CachingRealm.java" checks="LineLength" />

View File

@ -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<Attribute> attributes,
ActionListener<List<String>> 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<String> 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<Filter> 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<Filter> 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);

View File

@ -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<SearchResultEntry> listener) {
void searchForDN(LDAPConnection connection, String username, SecuredString password,
int timeLimitSeconds, ActionListener<SearchResultEntry> 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 <code>DOMAIN\\username</code> down-level credentials and this class contains the logic necessary
* to authenticate this form of a username
* Active Directory calls the format <code>DOMAIN\\username</code> down-level credentials and
* this class contains the logic necessary to authenticate this form of a username
*/
static class DownLevelADAuthenticator extends ADAuthenticator {
Cache<String, String> domainNameCache = CacheBuilder.<String, String>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<SearchResultEntry> results, String netBiosDomainName, Cache<String, String> domainNameCache,
static void handleSearchResults(List<SearchResultEntry> results, String netBiosDomainName,
Cache<String, String> domainNameCache,
ActionListener<String> listener) {
Optional<SearchResultEntry> entry = results.stream().filter((r) -> r.hasAttribute("ncname")).findFirst();
Optional<SearchResultEntry> 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);

View File

@ -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<SearchResultEntry> 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()));
}

View File

@ -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<String> BASE_DN = Setting.simpleString("group_search.base_dn", Setting.Property.NodeScope);
static final Setting<String> BASE_DN = Setting.simpleString("group_search.base_dn",
Setting.Property.NodeScope);
static final Setting<LdapSearchScope> SCOPE = new Setting<>("group_search.scope", (String) null,
s -> LdapSearchScope.resolve(s, LdapSearchScope.SUB_TREE), Setting.Property.NodeScope);
static final Setting<String> USER_ATTRIBUTE = Setting.simpleString("group_search.user_attribute", Setting.Property.NodeScope);
static final Setting<String> USER_ATTRIBUTE = Setting.simpleString(
"group_search.user_attribute", Setting.Property.NodeScope);
static final Setting<String> FILTER = new Setting<>("group_search.filter", GROUP_SEARCH_DEFAULT_FILTER,
Function.identity(), Setting.Property.NodeScope);
static final Setting<String> 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<Attribute> attributes, LDAPInterface connection, TimeValue timeout,
ActionListener<String> listener) {
private void getUserId(String dn, Collection<Attribute> attributes, LDAPInterface connection,
TimeValue timeout, ActionListener<String> 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<String> listener) {
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()),
void readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout,
ActionListener<String> 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);

View File

@ -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<String> 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 {

View File

@ -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> T privilegedConnect(CheckedSupplier<T, LDAPException> supplier) throws LDAPException {
public static <T> T privilegedConnect(CheckedSupplier<T, LDAPException> supplier)
throws LDAPException {
SpecialPermission.check();
try {
return AccessController.doPrivileged((PrivilegedExceptionAction<T>) 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<SearchResultEntry> listener, String... attributes) {
public static void searchForEntry(LDAPInterface ldap, String baseDN, SearchScope scope,
Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<SearchResultEntry> 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<SearchResultEntry> 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<SearchResultEntry> 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<SearchResultEntry> listener, String... attributes) {
public static void searchForEntry(LDAPConnectionPool ldap, String baseDN, SearchScope scope,
Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<SearchResultEntry> 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<List<SearchResultEntry>> listener, String... attributes) {
public static void search(LDAPInterface ldap, String baseDN, SearchScope scope,
Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<List<SearchResultEntry>> 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<List<SearchResultEntry>> 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<List<SearchResultEntry>> 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<List<SearchResultEntry>> listener, String... attributes) {
public static void search(LDAPConnectionPool ldap, String baseDN, SearchScope scope,
Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<List<SearchResultEntry>> 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 <code>true</code> if the provide {@link SearchResult} was successfully completed
* by the server.
* <strong>Note:</strong> Referrals are <em>not</em> 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<SearchResultEntry> listener, Filter filter) {
super(ldapConnection, ((asyncRequestID, searchResult) -> {
final List<SearchResultEntry> 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<SearchResultEntry> listener, Filter filter,
boolean ignoreReferralErrors) {
super(ldapConnection, ignoreReferralErrors, ActionListener.wrap(searchResult -> {
final List<SearchResultEntry> 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<SearchResultEntry> entryList = new ArrayList<>();
private final List<SearchResultReference> referenceList = new ArrayList<>();
protected final SetOnce<SearchRequest> searchRequestRef = new SetOnce<>();
private final BiConsumer<AsyncRequestID, SearchResult> consumer;
private final LDAPConnection ldapConnection;
private final boolean ignoreReferralErrors;
private final ActionListener<SearchResult> listener;
private final int depth;
LdapSearchResultListener(LDAPConnection ldapConnection, BiConsumer<AsyncRequestID, SearchResult> consumer, int depth) {
LdapSearchResultListener(LDAPConnection ldapConnection, boolean ignoreReferralErrors,
ActionListener<SearchResult> 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<String> referralUrlsList = new ArrayList<>(Arrays.asList(referralUrls));
BiConsumer<AsyncRequestID, SearchResult> 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<SearchResult> 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<AsyncRequestID, SearchResult> consumer, int depth,
SearchResult originatingResult, AsyncRequestID asyncRequestID) throws LDAPException {
private static void followReferral(LDAPConnection ldapConnection, String urlString,
SearchRequest searchRequest,
ActionListener<SearchResult> 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);
}
}
}
}

View File

@ -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.
* <p>
* A standard looking usage pattern could look like this:
* <pre>
@ -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<Boolean> 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<LdapSession> listener);
public abstract void session(String user, SecuredString password,
ActionListener<LdapSession> 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<Setting<?>> getSettings() {
Set<Setting<?>> 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;

View File

@ -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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<Filter> 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<Filter> 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<Filter> 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);
}

View File

@ -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<String> 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<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder(
containsString("Geniuses"),
@ -91,7 +97,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
PlainActionFuture<List<String>> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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();

View File

@ -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)

View File

@ -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<List<String>> 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));
}
}

View File

@ -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();
}