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[/\\]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[/\\]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[/\\]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[/\\]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[/\\]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[/\\]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[/\\]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[/\\]BCrypt.java" checks="LineLength" />
<suppress files="plugin[/\\]src[/\\]main[/\\]java[/\\]org[/\\]elasticsearch[/\\]xpack[/\\]security[/\\]authc[/\\]support[/\\]CachingRealm.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 static final String TOKEN_GROUPS = "tokenGroups";
private final String baseDn; private final String baseDn;
private final LdapSearchScope scope; 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.baseDn = settings.get("base_dn", baseDnDefault);
this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE); this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE);
this.ignoreReferralErrors = ignoreReferralErrors;
} }
@Override @Override
public void resolve(LDAPInterface connection, String userDn, TimeValue timeout, Logger logger, Collection<Attribute> attributes, public void resolve(LDAPInterface connection, String userDn, TimeValue timeout, Logger logger, Collection<Attribute> attributes,
ActionListener<List<String>> listener) { ActionListener<List<String>> listener) {
buildGroupQuery(connection, userDn, timeout, buildGroupQuery(connection, userDn, timeout,
ActionListener.wrap((filter) -> { ignoreReferralErrors, ActionListener.wrap((filter) -> {
if (filter == null) { if (filter == null) {
listener.onResponse(Collections.emptyList()); listener.onResponse(Collections.emptyList());
} else { } else {
logger.debug("group SID to DN [{}] search filter: [{}]", userDn, filter); 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) -> { ActionListener.wrap((results) -> {
List<String> groups = results.stream() List<String> groups = results.stream()
.map(SearchResultEntry::getDN) .map(SearchResultEntry::getDN)
@ -67,8 +71,10 @@ class ActiveDirectoryGroupsResolver implements GroupsResolver {
return null; return null;
} }
static void buildGroupQuery(LDAPInterface connection, String userDn, TimeValue timeout, ActionListener<Filter> listener) { static void buildGroupQuery(LDAPInterface connection, String userDn, TimeValue timeout,
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), boolean ignoreReferralErrors, ActionListener<Filter> listener) {
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER,
Math.toIntExact(timeout.seconds()), ignoreReferralErrors,
ActionListener.wrap((entry) -> { ActionListener.wrap((entry) -> {
if (entry == null || entry.hasAttribute(TOKEN_GROUPS) == false) { if (entry == null || entry.hasAttribute(TOKEN_GROUPS) == false) {
listener.onResponse(null); listener.onResponse(null);

View File

@ -65,13 +65,18 @@ class ActiveDirectorySessionFactory extends SessionFactory {
Settings settings = config.settings(); Settings settings = config.settings();
String domainName = settings.get(AD_DOMAIN_NAME_SETTING); String domainName = settings.get(AD_DOMAIN_NAME_SETTING);
if (domainName == null) { 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); String domainDN = buildDnFromDomain(domainName);
GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN); GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(
defaultADAuthenticator = new DefaultADAuthenticator(settings, timeout, logger, groupResolver, domainDN); settings.getAsSettings("group_search"), domainDN, ignoreReferralErrors);
downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout, logger, groupResolver, domainDN, sslService); defaultADAuthenticator = new DefaultADAuthenticator(settings, timeout,
upnADAuthenticator = new UpnADAuthenticator(settings, timeout, logger, groupResolver, domainDN); ignoreReferralErrors, logger, groupResolver, domainDN);
downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout,
ignoreReferralErrors, logger, groupResolver, domainDN, sslService);
upnADAuthenticator = new UpnADAuthenticator(settings, timeout,
ignoreReferralErrors, logger, groupResolver, domainDN);
} }
@Override @Override
@ -135,13 +140,16 @@ class ActiveDirectorySessionFactory extends SessionFactory {
abstract static class ADAuthenticator { abstract static class ADAuthenticator {
final TimeValue timeout; final TimeValue timeout;
final boolean ignoreReferralErrors;
final Logger logger; final Logger logger;
final GroupsResolver groupsResolver; final GroupsResolver groupsResolver;
final String userSearchDN; final String userSearchDN;
final LdapSearchScope userSearchScope; 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.timeout = timeout;
this.ignoreReferralErrors = ignoreReferralErrors;
this.logger = logger; this.logger = logger;
this.groupsResolver = groupsResolver; this.groupsResolver = groupsResolver;
userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN); userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN);
@ -195,19 +203,22 @@ class ActiveDirectorySessionFactory extends SessionFactory {
final String userSearchFilter; final String userSearchFilter;
final String domainName; final String domainName;
DefaultADAuthenticator(Settings settings, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN) { DefaultADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors,
super(settings, timeout, logger, groupsResolver, domainDN); Logger logger, GroupsResolver groupsResolver, String domainDN) {
super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN);
domainName = settings.get(AD_DOMAIN_NAME_SETTING); domainName = settings.get(AD_DOMAIN_NAME_SETTING);
userSearchFilter = settings.get(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})" + userSearchFilter = settings.get(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})" +
"(userPrincipalName={0}@" + domainName + ")))"); "(userPrincipalName={0}@" + domainName + ")))");
} }
@Override @Override
void searchForDN(LDAPConnection connection, String username, SecuredString password, int timeLimitSeconds, void searchForDN(LDAPConnection connection, String username, SecuredString password,
ActionListener<SearchResultEntry> listener) { int timeLimitSeconds, ActionListener<SearchResultEntry> listener) {
try { try {
searchForEntry(connection, userSearchDN, userSearchScope.scope(), createFilter(userSearchFilter, username), searchForEntry(connection, userSearchDN, userSearchScope.scope(),
timeLimitSeconds, listener, attributesToSearchFor(groupsResolver.attributes())); createFilter(userSearchFilter, username), timeLimitSeconds,
ignoreReferralErrors, listener,
attributesToSearchFor(groupsResolver.attributes()));
} catch (LDAPException e) { } catch (LDAPException e) {
listener.onFailure(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 * Active Directory calls the format <code>DOMAIN\\username</code> down-level credentials and
* to authenticate this form of a username * this class contains the logic necessary to authenticate this form of a username
*/ */
static class DownLevelADAuthenticator extends ADAuthenticator { static class DownLevelADAuthenticator extends ADAuthenticator {
Cache<String, String> domainNameCache = CacheBuilder.<String, String>builder().setMaximumWeight(100).build(); Cache<String, String> domainNameCache = CacheBuilder.<String, String>builder().setMaximumWeight(100).build();
@ -231,9 +242,12 @@ class ActiveDirectorySessionFactory extends SessionFactory {
final SSLService sslService; final SSLService sslService;
final RealmConfig config; 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) { SSLService sslService) {
super(config.settings(), timeout, logger, groupsResolver, domainDN); super(config.settings(), timeout, ignoreReferralErrors, logger, groupsResolver,
domainDN);
this.domainDN = domainDN; this.domainDN = domainDN;
this.settings = config.settings(); this.settings = config.settings();
this.sslService = sslService; this.sslService = sslService;
@ -255,8 +269,9 @@ class ActiveDirectorySessionFactory extends SessionFactory {
} else { } else {
try { try {
searchForEntry(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), searchForEntry(connection, domainDN, LdapSearchScope.SUB_TREE.scope(),
createFilter("(&(objectClass=user)(sAMAccountName={0}))", accountName), timeLimitSeconds, listener, createFilter("(&(objectClass=user)(sAMAccountName={0}))",
attributesToSearchFor(groupsResolver.attributes())); accountName), timeLimitSeconds, ignoreReferralErrors,
listener, attributesToSearchFor(groupsResolver.attributes()));
} catch (LDAPException e) { } catch (LDAPException e) {
IOUtils.closeWhileHandlingException(connection); IOUtils.closeWhileHandlingException(connection);
listener.onFailure(e); listener.onFailure(e);
@ -274,8 +289,9 @@ class ActiveDirectorySessionFactory extends SessionFactory {
if (cachedName != null) { if (cachedName != null) {
listener.onResponse(cachedName); listener.onResponse(cachedName);
} else if (usingGlobalCatalog(settings, connection)) { } 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 // the global catalog does not replicate the necessary information to map a netbios
// connect to the normal ports. This code uses the standard ports to avoid adding even more settings and is probably ok as // 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 // most AD users do not use non-standard ports
final LDAPConnectionOptions options = connectionOptions(config, sslService, logger); final LDAPConnectionOptions options = connectionOptions(config, sslService, logger);
boolean startedSearching = false; boolean startedSearching = false;
@ -283,7 +299,8 @@ class ActiveDirectorySessionFactory extends SessionFactory {
try { try {
Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName);
if (connection.getSSLSession() != null) { if (connection.getSSLSession() != null) {
searchConnection = LdapUtils.privilegedConnect(() -> new LDAPConnection(connection.getSocketFactory(), options, searchConnection = LdapUtils.privilegedConnect(
() -> new LDAPConnection(connection.getSocketFactory(), options,
connection.getConnectedAddress(), 636)); connection.getConnectedAddress(), 636));
} else { } else {
searchConnection = LdapUtils.privilegedConnect(() -> searchConnection = LdapUtils.privilegedConnect(() ->
@ -291,14 +308,16 @@ class ActiveDirectorySessionFactory extends SessionFactory {
} }
searchConnection.bind(username, new String(password.internalChars())); searchConnection.bind(username, new String(password.internalChars()));
final LDAPConnection finalConnection = searchConnection; final LDAPConnection finalConnection = searchConnection;
search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter,
ActionListener.wrap((results) -> { timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap(
IOUtils.close(finalConnection); (results) -> {
handleSearchResults(results, netBiosDomainName, domainNameCache, listener); IOUtils.close(finalConnection);
}, (e) -> { handleSearchResults(results, netBiosDomainName,
IOUtils.closeWhileHandlingException(connection); domainNameCache, listener);
listener.onFailure(e); }, (e) -> {
}), IOUtils.closeWhileHandlingException(connection);
listener.onFailure(e);
}),
"ncname"); "ncname");
startedSearching = true; startedSearching = true;
} catch (LDAPException e) { } catch (LDAPException e) {
@ -311,8 +330,10 @@ class ActiveDirectorySessionFactory extends SessionFactory {
} else { } else {
try { try {
Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName);
search(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, search(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter,
ActionListener.wrap((results) -> handleSearchResults(results, netBiosDomainName, domainNameCache, listener), timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap(
(results) -> handleSearchResults(results, netBiosDomainName,
domainNameCache, listener),
(e) -> { (e) -> {
IOUtils.closeWhileHandlingException(connection); IOUtils.closeWhileHandlingException(connection);
listener.onFailure(e); 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) { 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()) { if (entry.isPresent()) {
final String value = entry.get().getAttributeValue("ncname"); final String value = entry.get().getAttributeValue("ncname");
try { try {
@ -353,8 +377,9 @@ class ActiveDirectorySessionFactory extends SessionFactory {
private static final String UPN_USER_FILTER = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={1})))"; private static final String UPN_USER_FILTER = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={1})))";
UpnADAuthenticator(Settings settings, TimeValue timeout, Logger logger, GroupsResolver groupsResolver, String domainDN) { UpnADAuthenticator(Settings settings, TimeValue timeout, boolean ignoreReferralErrors,
super(settings, timeout, logger, groupsResolver, domainDN); Logger logger, GroupsResolver groupsResolver, String domainDN) {
super(settings, timeout, ignoreReferralErrors, logger, groupsResolver, domainDN);
} }
void searchForDN(LDAPConnection connection, String username, SecuredString password, int timeLimitSeconds, void searchForDN(LDAPConnection connection, String username, SecuredString password, int timeLimitSeconds,
@ -366,7 +391,8 @@ class ActiveDirectorySessionFactory extends SessionFactory {
final String domainDN = buildDnFromDomain(domainName); final String domainDN = buildDnFromDomain(domainName);
try { try {
Filter filter = createFilter(UPN_USER_FILTER, accountName, username); 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())); attributesToSearchFor(groupsResolver.attributes()));
} catch (LDAPException e) { } catch (LDAPException e) {
listener.onFailure(e); listener.onFailure(e);

View File

@ -16,7 +16,6 @@ import com.unboundid.ldap.sdk.ServerSet;
import com.unboundid.ldap.sdk.SimpleBindRequest; import com.unboundid.ldap.sdk.SimpleBindRequest;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.lucene.util.IOUtils; import org.apache.lucene.util.IOUtils;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionListener;
import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
@ -294,7 +293,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
private void findUser(String user, LDAPInterface ldapInterface, ActionListener<SearchResultEntry> listener) { private void findUser(String user, LDAPInterface ldapInterface, ActionListener<SearchResultEntry> listener) {
searchForEntry(ldapInterface, userSearchBaseDn, scope.scope(), 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())); attributesToSearchFor(groupResolver.attributes()));
} }

View File

@ -19,6 +19,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSearchScope; 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.LdapSession.GroupsResolver;
import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; 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.createFilter;
import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.search; 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.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 * Resolves the groups for a user by executing a search with a filter usually that contains a group
* matches an ID of the user * object class with a attribute that matches an ID of the user
*/ */
class SearchGroupsResolver implements GroupsResolver { class SearchGroupsResolver implements GroupsResolver {
private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" + 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})))"; "(|(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, static final Setting<LdapSearchScope> SCOPE = new Setting<>("group_search.scope", (String) null,
s -> LdapSearchScope.resolve(s, LdapSearchScope.SUB_TREE), Setting.Property.NodeScope); 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, static final Setting<String> FILTER = new Setting<>("group_search.filter",
Function.identity(), Setting.Property.NodeScope); GROUP_SEARCH_DEFAULT_FILTER, Function.identity(), Setting.Property.NodeScope);
private final String baseDn; private final String baseDn;
private final String filter; private final String filter;
private final String userAttribute; private final String userAttribute;
private final LdapSearchScope scope; private final LdapSearchScope scope;
private final boolean ignoreReferralErrors;
SearchGroupsResolver(Settings settings) { SearchGroupsResolver(Settings settings) {
if (BASE_DN.exists(settings)) { if (BASE_DN.exists(settings)) {
@ -66,6 +72,7 @@ class SearchGroupsResolver implements GroupsResolver {
filter = FILTER.get(settings); filter = FILTER.get(settings);
userAttribute = USER_ATTRIBUTE.get(settings); userAttribute = USER_ATTRIBUTE.get(settings);
scope = SCOPE.get(settings); scope = SCOPE.get(settings);
this.ignoreReferralErrors = IGNORE_REFERRAL_ERRORS_SETTING.get(settings);
} }
@Override @Override
@ -77,9 +84,14 @@ class SearchGroupsResolver implements GroupsResolver {
} else { } else {
try { try {
Filter userFilter = createFilter(filter, userId); 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( 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), listener::onFailure),
SearchRequest.NO_ATTRIBUTES); SearchRequest.NO_ATTRIBUTES);
} catch (LDAPException e) { } catch (LDAPException e) {
@ -96,12 +108,13 @@ class SearchGroupsResolver implements GroupsResolver {
return null; return null;
} }
private void getUserId(String dn, Collection<Attribute> attributes, LDAPInterface connection, TimeValue timeout, private void getUserId(String dn, Collection<Attribute> attributes, LDAPInterface connection,
ActionListener<String> listener) { TimeValue timeout, ActionListener<String> listener) {
if (isNullOrEmpty(userAttribute)) { if (isNullOrEmpty(userAttribute)) {
listener.onResponse(dn); listener.onResponse(dn);
} else if (attributes != null) { } 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) .map(Attribute::getValue)
.findFirst() .findFirst()
.orElse(null); .orElse(null);
@ -111,8 +124,10 @@ class SearchGroupsResolver implements GroupsResolver {
} }
} }
void readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout, ActionListener<String> listener) { void readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout,
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), ActionListener<String> listener) {
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER,
Math.toIntExact(timeout.seconds()), ignoreReferralErrors,
ActionListener.wrap((entry) -> { ActionListener.wrap((entry) -> {
if (entry == null || entry.hasAttribute(userAttribute) == false) { if (entry == null || entry.hasAttribute(userAttribute) == false) {
listener.onResponse(null); 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.OBJECT_CLASS_PRESENCE_FILTER;
import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry; 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 * 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", private static final Setting<String> ATTRIBUTE = new Setting<>("user_group_attribute", "memberOf",
Function.identity(), Setting.Property.NodeScope); Function.identity(), Setting.Property.NodeScope);
private final String attribute; private final String attribute;
private final boolean ignoreReferralErrors;
UserAttributeGroupsResolver(Settings settings) { 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.attribute = Objects.requireNonNull(attribute);
this.ignoreReferralErrors = ignoreReferralErrors;
} }
@Override @Override
@ -53,7 +56,7 @@ class UserAttributeGroupsResolver implements GroupsResolver {
listener.onResponse(Collections.unmodifiableList(list)); listener.onResponse(Collections.unmodifiableList(list));
} else { } else {
searchForEntry(connection, userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, Math.toIntExact(timeout.seconds()), 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) { if (entry == null || entry.hasAttribute(attribute) == false) {
listener.onResponse(Collections.emptyList()); listener.onResponse(Collections.emptyList());
} else { } else {

View File

@ -44,12 +44,14 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.function.BiConsumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public final class LdapUtils { 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() { 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(); SpecialPermission.check();
try { try {
return AccessController.doPrivileged((PrivilegedExceptionAction<T>) supplier::get); 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 * 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, public static void searchForEntry(LDAPInterface ldap, String baseDN, SearchScope scope,
ActionListener<SearchResultEntry> listener, String... attributes) { Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<SearchResultEntry> listener,
String... attributes) {
if (ldap instanceof LDAPConnection) { 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) { } 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 { } else {
throw new IllegalArgumentException("unsupported LDAPInterface implementation: " + ldap); 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 * This method performs an asynchronous ldap search operation that only expects at most one
* then this is an error. If no results are found, then {@code null} will be returned. * 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, public static void searchForEntry(LDAPConnection ldap, String baseDN, SearchScope scope,
ActionListener<SearchResultEntry> listener, String... attributes) { Filter filter, int timeLimitSeconds,
LdapSearchResultListener searchResultListener = new SingleEntryListener(ldap, listener, filter); boolean ignoreReferralErrors,
ActionListener<SearchResultEntry> listener,
String... attributes) {
LdapSearchResultListener searchResultListener = new SingleEntryListener(ldap, listener,
filter, ignoreReferralErrors);
try { try {
SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope,
false, filter, attributes); DereferencePolicy.NEVER, 0, timeLimitSeconds, false, filter, attributes);
searchResultListener.setSearchRequest(request); searchResultListener.setSearchRequest(request);
ldap.asyncSearch(request); ldap.asyncSearch(request);
} catch (LDAPException e) { } 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 * This method performs an asynchronous ldap search operation that only expects at most one
* then this is an error. If no results are found, then {@code null} will be returned. * 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, public static void searchForEntry(LDAPConnectionPool ldap, String baseDN, SearchScope scope,
ActionListener<SearchResultEntry> listener, String... attributes) { Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<SearchResultEntry> listener,
String... attributes) {
boolean searching = false; boolean searching = false;
LDAPConnection ldapConnection = null; LDAPConnection ldapConnection = null;
try { try {
ldapConnection = privilegedConnect(ldap::getConnection); ldapConnection = privilegedConnect(ldap::getConnection);
final LDAPConnection finalConnection = ldapConnection; final LDAPConnection finalConnection = ldapConnection;
searchForEntry(finalConnection, baseDN, scope, filter, timeLimitSeconds, ActionListener.wrap( searchForEntry(finalConnection, baseDN, scope, filter, timeLimitSeconds,
(entry) -> { ignoreReferralErrors, ActionListener.wrap(
IOUtils.close(() -> ldap.releaseConnection(finalConnection)); (entry) -> {
listener.onResponse(entry); IOUtils.close(() -> ldap.releaseConnection(finalConnection));
}, listener.onResponse(entry);
(e) -> { },
IOUtils.closeWhileHandlingException(() -> ldap.releaseConnection(finalConnection)); (e) -> {
listener.onFailure(e); IOUtils.closeWhileHandlingException(
}), attributes); () -> ldap.releaseConnection(finalConnection)
);
listener.onFailure(e);
}), attributes);
searching = true; searching = true;
} catch (LDAPException e) { } catch (LDAPException e) {
listener.onFailure(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 * 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, public static void search(LDAPInterface ldap, String baseDN, SearchScope scope,
ActionListener<List<SearchResultEntry>> listener, String... attributes) { Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<List<SearchResultEntry>> listener,
String... attributes) {
if (ldap instanceof LDAPConnection) { 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) { } 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 { } else {
throw new IllegalArgumentException("unsupported LDAPInterface implementation: " + ldap); 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 * 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, public static void search(LDAPConnection ldap, String baseDN, SearchScope scope,
ActionListener<List<SearchResultEntry>> listener, String... attributes) { Filter filter, int timeLimitSeconds,
LdapSearchResultListener searchResultListener = new LdapSearchResultListener(ldap, boolean ignoreReferralErrors,
(asyncRequestID, searchResult) -> listener.onResponse(Collections.unmodifiableList(searchResult.getSearchEntries())), 1); 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 { try {
SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, SearchRequest request = new SearchRequest(searchResultListener, baseDN, scope,
false, filter, attributes); DereferencePolicy.NEVER, 0, timeLimitSeconds, false, filter, attributes);
searchResultListener.setSearchRequest(request); searchResultListener.setSearchRequest(request);
ldap.asyncSearch(request); ldap.asyncSearch(request);
} catch (LDAPException e) { } catch (LDAPException e) {
@ -177,20 +217,35 @@ public final class LdapUtils {
/** /**
* This method performs an asynchronous ldap search operation that could have multiple results * 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, public static void search(LDAPConnectionPool ldap, String baseDN, SearchScope scope,
ActionListener<List<SearchResultEntry>> listener, String... attributes) { Filter filter, int timeLimitSeconds,
boolean ignoreReferralErrors,
ActionListener<List<SearchResultEntry>> listener,
String... attributes) {
boolean searching = false; boolean searching = false;
LDAPConnection ldapConnection = null; LDAPConnection ldapConnection = null;
try { try {
ldapConnection = ldap.getConnection(); ldapConnection = ldap.getConnection();
final LDAPConnection finalConnection = ldapConnection; final LDAPConnection finalConnection = ldapConnection;
LdapSearchResultListener ldapSearchResultListener = new LdapSearchResultListener(ldapConnection, LdapSearchResultListener ldapSearchResultListener = new LdapSearchResultListener(
(asyncRequestID, searchResult) -> { ldapConnection, ignoreReferralErrors,
IOUtils.closeWhileHandlingException(() -> ldap.releaseConnection(finalConnection)); ActionListener.wrap(
listener.onResponse(Collections.unmodifiableList(searchResult.getSearchEntries())); searchResult -> {
}, 1); IOUtils.closeWhileHandlingException(
SearchRequest request = new SearchRequest(ldapSearchResultListener, baseDN, scope, DereferencePolicy.NEVER, 0, timeLimitSeconds, () -> ldap.releaseConnection(finalConnection)
false, filter, attributes); );
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); ldapSearchResultListener.setSearchRequest(request);
finalConnection.asyncSearch(request); finalConnection.asyncSearch(request);
searching = true; 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), * Returns <code>true</code> if the provide {@link SearchResult} was successfully completed
new StringBuffer(), null).toString()); * 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) { public static String[] attributesToSearchFor(String[] attributes) {
@ -222,35 +319,41 @@ public final class LdapUtils {
private static class SingleEntryListener extends LdapSearchResultListener { private static class SingleEntryListener extends LdapSearchResultListener {
SingleEntryListener(LDAPConnection ldapConnection, ActionListener<SearchResultEntry> listener, Filter filter) { SingleEntryListener(LDAPConnection ldapConnection,
super(ldapConnection, ((asyncRequestID, searchResult) -> { ActionListener<SearchResultEntry> listener, Filter filter,
final List<SearchResultEntry> entryList = searchResult.getSearchEntries(); boolean ignoreReferralErrors) {
if (entryList.size() > 1) { super(ldapConnection, ignoreReferralErrors, ActionListener.wrap(searchResult -> {
listener.onFailure(Exceptions.authenticationError("multiple search results found for [{}]", filter)); final List<SearchResultEntry> entryList = searchResult.getSearchEntries();
} else if (entryList.size() == 1) { if (entryList.size() > 1) {
listener.onResponse(entryList.get(0)); listener.onFailure(Exceptions.authenticationError(
} else { "multiple search results found for [{}]", filter));
listener.onResponse(null); } else if (entryList.size() == 1) {
} listener.onResponse(entryList.get(0));
}), 1); } else {
listener.onResponse(null);
}
}, listener::onFailure)
, 1);
} }
} }
private static class LdapSearchResultListener implements AsyncSearchResultListener { 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<SearchResultEntry> entryList = new ArrayList<>();
private final List<SearchResultReference> referenceList = new ArrayList<>(); private final List<SearchResultReference> referenceList = new ArrayList<>();
protected final SetOnce<SearchRequest> searchRequestRef = new SetOnce<>(); protected final SetOnce<SearchRequest> searchRequestRef = new SetOnce<>();
private final BiConsumer<AsyncRequestID, SearchResult> consumer;
private final LDAPConnection ldapConnection; private final LDAPConnection ldapConnection;
private final boolean ignoreReferralErrors;
private final ActionListener<SearchResult> listener;
private final int depth; 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.ldapConnection = ldapConnection;
this.consumer = consumer; this.listener = listener;
this.depth = depth; this.depth = depth;
this.ignoreReferralErrors = ignoreReferralErrors;
} }
@Override @Override
@ -265,72 +368,97 @@ public final class LdapUtils {
@Override @Override
public void searchResultReceived(AsyncRequestID requestID, SearchResult searchResult) { 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 // Whenever we get a search result we need to check for a referral.
// an object stored in a different LDAP server/partition. There are cases where we need to follow a referral in order to get // A referral is a mechanism for an LDAP server to reference an object stored in a
// the actual object we are searching for // 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() final String[] referralUrls = referenceList.stream()
.flatMap((ref) -> Arrays.stream(ref.getReferralURLs())) .flatMap((ref) -> Arrays.stream(ref.getReferralURLs()))
.collect(Collectors.toList()) .collect(Collectors.toList())
.toArray(Strings.EMPTY_ARRAY); .toArray(Strings.EMPTY_ARRAY);
final SearchRequest searchRequest = searchRequestRef.get(); final SearchRequest request = searchRequestRef.get();
if (referralUrls.length == 0 || searchRequest.followReferrals(ldapConnection) == false) { 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 // either no referrals to follow or we have explicitly disabled referral following
// a new search result that has the values we've collected. The search result passed to this method will not have of the // on the connection so we just create a new search result that has the values we've
// entries as we are using a result listener and the results are not being collected by the LDAP library // collected. The search result passed to this method will not have of the entries
LOGGER.trace("LDAP Search {} => {} ({})", searchRequest, searchResult, entryList); // as we are using a result listener and the results are not being collected by the
SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), searchResult.getResultCode(), searchResult // LDAP library
.getDiagnosticMessage(), searchResult.getMatchedDN(), referralUrls, entryList, referenceList, entryList.size(), LOGGER.trace("LDAP Search {} => {} ({})", request, searchResult, entryList);
referenceList.size(), searchResult.getResponseControls()); if (isSuccess(searchResult)) {
consumer.accept(requestID, resultWithValues); 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()) { } 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 // we've gone through too many levels of referrals so we terminate with the values
// code to indicate the search was terminated early // collected so far and the proper result code to indicate the search was
LOGGER.trace("Referral limit exceeded {} => {} ({})", searchRequest, searchResult, entryList); // terminated early
SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), ResultCode.REFERRAL_LIMIT_EXCEEDED, LOGGER.trace("Referral limit exceeded {} => {} ({})",
searchResult.getDiagnosticMessage(), searchResult.getMatchedDN(), referralUrls, entryList, referenceList, request, searchResult, entryList);
entryList.size(), referenceList.size(), searchResult.getResponseControls()); listener.onFailure(new LDAPException(ResultCode.REFERRAL_LIMIT_EXCEEDED,
consumer.accept(requestID, resultWithValues); "Referral limit exceeded (" + depth + ")",
searchResult.getMatchedDN(), referralUrls,
searchResult.getResponseControls()));
} else { } else {
if (LOGGER.isTraceEnabled()) { 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 // there are referrals to follow, so we start the process to follow the referrals
final CountDown countDown = new CountDown(referralUrls.length); final CountDown countDown = new CountDown(referralUrls.length);
final List<String> referralUrlsList = new ArrayList<>(Arrays.asList(referralUrls)); final List<String> referralUrlsList = new ArrayList<>(Arrays.asList(referralUrls));
BiConsumer<AsyncRequestID, SearchResult> referralConsumer = (reqID, innerResult) -> { ActionListener<SearchResult> referralListener = ActionListener.wrap(
// synchronize here since we are possibly sending out a lot of requests and the result lists are not thread safe and innerResult -> {
// this also provides us with a consistent view // synchronize here since we are possibly sending out a lot of requests
synchronized (this) { // and the result lists are not thread safe and this also provides us
if (innerResult.getSearchEntries() != null) { // with a consistent view
entryList.addAll(innerResult.getSearchEntries()); synchronized (this) {
} if (innerResult.getSearchEntries() != null) {
if (innerResult.getSearchReferences() != null) { entryList.addAll(innerResult.getSearchEntries());
referenceList.addAll(innerResult.getSearchReferences()); }
} if (innerResult.getSearchReferences() != null) {
} referenceList.addAll(innerResult.getSearchReferences());
}
}
// count down and once all referrals have been traversed then we can create the results // count down and once all referrals have been traversed then we can
if (countDown.countDown()) { // create the results
SearchResult resultWithValues = new SearchResult(searchResult.getMessageID(), searchResult.getResultCode(), if (countDown.countDown()) {
searchResult.getDiagnosticMessage(), searchResult.getMatchedDN(), SearchResult resultWithValues = new SearchResult(
referralUrlsList.toArray(Strings.EMPTY_ARRAY), entryList, referenceList, searchResult.getMessageID(), searchResult.getResultCode(),
entryList.size(), referenceList.size(), searchResult.getResponseControls()); searchResult.getDiagnosticMessage(),
consumer.accept(requestID, resultWithValues); searchResult.getMatchedDN(),
} referralUrlsList.toArray(Strings.EMPTY_ARRAY), entryList,
}; referenceList, entryList.size(), referenceList.size(),
searchResult.getResponseControls());
listener.onResponse(resultWithValues);
}
}, listener::onFailure);
for (String referralUrl : referralUrls) { for (String referralUrl : referralUrls) {
try { try {
// for each referral follow it and any other referrals returned until we get to a depth that is greater than or // for each referral follow it and any other referrals returned until we
// equal to the referral hop limit or all referrals have been followed. Each time referrals are followed from a // get to a depth that is greater than or equal to the referral hop limit
// search result, the depth increases by 1 // or all referrals have been followed. Each time referrals are followed
followReferral(ldapConnection, referralUrl, searchRequest, referralConsumer, depth + 1, searchResult, requestID); // from a search result, the depth increases by 1
followReferral(ldapConnection, referralUrl, request, referralListener,
depth + 1, ignoreReferralErrors, searchResult);
} catch (LDAPException e) { } catch (LDAPException e) {
LOGGER.warn((Supplier<?>) LOGGER.warn((Supplier<?>) () -> new ParameterizedMessage(
() -> new ParameterizedMessage("caught exception while trying to follow referral [{}]", referralUrl), e); "caught exception while trying to follow referral [{}]",
referralConsumer.accept(requestID, new SearchResult(searchResult.getMessageID(), e.getResultCode(), referralUrl), e);
e.getDiagnosticMessage(), e.getMatchedDN(), e.getReferralURLs(), 0, 0, e.getResponseControls())); 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 * Performs the actual connection and following of a referral given a URL string.
* result that is relevant to our search * 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, private static void followReferral(LDAPConnection ldapConnection, String urlString,
BiConsumer<AsyncRequestID, SearchResult> consumer, int depth, SearchRequest searchRequest,
SearchResult originatingResult, AsyncRequestID asyncRequestID) throws LDAPException { ActionListener<SearchResult> listener, int depth,
boolean ignoreErrors, SearchResult originatingResult)
throws LDAPException {
final LDAPURL referralURL = new LDAPURL(urlString); final LDAPURL referralURL = new LDAPURL(urlString);
final String host = referralURL.getHost(); final String host = referralURL.getHost();
// the host must be present in order to follow a referral // the host must be present in order to follow a referral
if (host != null) { if (host == null) {
// the referral URL often contains information necessary about the LDAP request such as the base DN, scope, and filter. If it // nothing to really do since a null host cannot really be handled, so we treat it as
// does not, then we reuse the values from the originating search request // an error
final String requestBaseDN; throw new LDAPException(ResultCode.UNAVAILABLE, "Null referral host in " + urlString);
if (referralURL.baseDNProvided()) { }
requestBaseDN = referralURL.getBaseDN().toString();
} else {
requestBaseDN = searchRequest.getBaseDN();
}
final SearchScope requestScope; // the referral URL often contains information necessary about the LDAP request such as
if (referralURL.scopeProvided()) { // the base DN, scope, and filter. If it does not, then we reuse the values from the
requestScope = referralURL.getScope(); // originating search request
} else { final String requestBaseDN;
requestScope = searchRequest.getScope(); if (referralURL.baseDNProvided()) {
} requestBaseDN = referralURL.getBaseDN().toString();
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);
}
}
} else { } else {
// nothing to really do since a null host cannot really be handled, so we just return with a response that is empty... requestBaseDN = searchRequest.getBaseDN();
consumer.accept(asyncRequestID, new SearchResult(originatingResult.getMessageID(), ResultCode.UNAVAILABLE, }
null, null, null, Collections.emptyList(), Collections.emptyList(), 0, 0, null));
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. * 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> * <p>
* A standard looking usage pattern could look like this: * A standard looking usage pattern could look like this:
* <pre> * <pre>
@ -52,9 +53,14 @@ public abstract class SessionFactory {
public static final String TIMEOUT_LDAP_SETTING = "timeout.ldap_search"; public static final String TIMEOUT_LDAP_SETTING = "timeout.ldap_search";
public static final String HOSTNAME_VERIFICATION_SETTING = "hostname_verification"; public static final String HOSTNAME_VERIFICATION_SETTING = "hostname_verification";
public static final String FOLLOW_REFERRALS_SETTING = "follow_referrals"; 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); 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_LDAPS = Pattern.compile("^ldaps:.*",
private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*", Pattern.CASE_INSENSITIVE); Pattern.CASE_INSENSITIVE);
private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*",
Pattern.CASE_INSENSITIVE);
protected final Logger logger; protected final Logger logger;
protected final RealmConfig config; protected final RealmConfig config;
@ -63,36 +69,43 @@ public abstract class SessionFactory {
protected final ServerSet serverSet; protected final ServerSet serverSet;
protected final boolean sslUsed; protected final boolean sslUsed;
protected final boolean ignoreReferralErrors;
protected SessionFactory(RealmConfig config, SSLService sslService) { protected SessionFactory(RealmConfig config, SSLService sslService) {
this.config = config; this.config = config;
this.logger = config.logger(getClass()); 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) { 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.millis());
searchTimeout = TimeValue.timeValueSeconds(1L); searchTimeout = TimeValue.timeValueSeconds(1L);
} }
this.timeout = searchTimeout; this.timeout = searchTimeout;
this.sslService = sslService; this.sslService = sslService;
LDAPServers ldapServers = ldapServers(config.settings()); LDAPServers ldapServers = ldapServers(settings);
this.serverSet = serverSet(config, sslService, ldapServers); this.serverSet = serverSet(config, sslService, ldapServers);
this.sslUsed = ldapServers.ssl; 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 * Authenticates the given user and opens a new connection that bound to it (meaning, all
* under the returned connection will be executed on behalf of the authenticated user. * 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 user The name of the user to authenticate the connection with.
* @param password The password of the user * @param password The password of the user
* @param listener the listener to call on a failure or result * @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 * Returns a flag to indicate if this session factory supports unauthenticated sessions.
* be established without providing any credentials in a call to {@link #unauthenticatedSession(String, ActionListener)} * 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 * @return true if the factory supports unauthenticated sessions
*/ */
@ -110,29 +123,43 @@ public abstract class SessionFactory {
throw new UnsupportedOperationException("unauthenticated sessions are not supported"); 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(); Settings realmSettings = config.settings();
LDAPConnectionOptions options = new LDAPConnectionOptions(); 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.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); options.setAllowConcurrentSocketFactoryUse(true);
SSLConfigurationSettings sslConfigurationSettings = SSLConfigurationSettings.withoutPrefix();
final SSLConfigurationSettings sslConfigurationSettings =
SSLConfigurationSettings.withoutPrefix();
final Settings realmSSLSettings = realmSettings.getByPrefix("ssl."); final Settings realmSSLSettings = realmSettings.getByPrefix("ssl.");
final boolean verificationModeExists = sslConfigurationSettings.verificationMode.exists(realmSSLSettings); final boolean verificationModeExists =
final boolean hostnameVerficationExists = realmSettings.get(HOSTNAME_VERIFICATION_SETTING, null) != null; sslConfigurationSettings.verificationMode.exists(realmSSLSettings);
final boolean hostnameVerficationExists =
realmSettings.get(HOSTNAME_VERIFICATION_SETTING, null) != null;
if (verificationModeExists && hostnameVerficationExists) { if (verificationModeExists && hostnameVerficationExists) {
throw new IllegalArgumentException("[" + HOSTNAME_VERIFICATION_SETTING + "] and [" + 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) { } else if (verificationModeExists) {
VerificationMode verificationMode = sslService.getVerificationMode(realmSSLSettings, Settings.EMPTY); VerificationMode verificationMode = sslService.getVerificationMode(realmSSLSettings,
Settings.EMPTY);
if (verificationMode == VerificationMode.FULL) { if (verificationMode == VerificationMode.FULL) {
options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true)); options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true));
} }
} else if (hostnameVerficationExists) { } else if (hostnameVerficationExists) {
new DeprecationLogger(logger).deprecated("the setting [{}] has been deprecated and will be removed in a future version. use " + new DeprecationLogger(logger).deprecated("the setting [{}] has been deprecated and " +
"[{}] instead", RealmSettings.getFullSettingKey(config, HOSTNAME_VERIFICATION_SETTING), "will be removed in a future version. use [{}] instead",
RealmSettings.getFullSettingKey(config, "ssl." + sslConfigurationSettings.verificationMode.getKey())); RealmSettings.getFullSettingKey(config, HOSTNAME_VERIFICATION_SETTING),
RealmSettings.getFullSettingKey(config, "ssl." +
sslConfigurationSettings.verificationMode.getKey()));
if (realmSettings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) { if (realmSettings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true)); options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true));
} }
@ -146,7 +173,8 @@ public abstract class SessionFactory {
// Parse LDAP urls // Parse LDAP urls
String[] ldapUrls = settings.getAsArray(URLS_SETTING, getDefaultLdapUrls(settings)); String[] ldapUrls = settings.getAsArray(URLS_SETTING, getDefaultLdapUrls(settings));
if (ldapUrls == null || ldapUrls.length == 0) { 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); return new LDAPServers(ldapUrls);
} }
@ -155,7 +183,8 @@ public abstract class SessionFactory {
return null; return null;
} }
private ServerSet serverSet(RealmConfig realmConfig, SSLService clientSSLService, LDAPServers ldapServers) { private ServerSet serverSet(RealmConfig realmConfig, SSLService clientSSLService,
LDAPServers ldapServers) {
Settings settings = realmConfig.settings(); Settings settings = realmConfig.settings();
SocketFactory socketFactory = null; SocketFactory socketFactory = null;
if (ldapServers.ssl()) { if (ldapServers.ssl()) {
@ -166,8 +195,8 @@ public abstract class SessionFactory {
logger.debug("using encryption for LDAP connections without hostname verification"); logger.debug("using encryption for LDAP connections without hostname verification");
} }
} }
return LdapLoadBalancing.serverSet(ldapServers.addresses(), ldapServers.ports(), settings, socketFactory, return LdapLoadBalancing.serverSet(ldapServers.addresses(), ldapServers.ports(), settings,
connectionOptions(realmConfig, sslService, logger)); socketFactory, connectionOptions(realmConfig, sslService, logger));
} }
// package private to use for testing // package private to use for testing
@ -182,12 +211,19 @@ public abstract class SessionFactory {
protected static Set<Setting<?>> getSettings() { protected static Set<Setting<?>> getSettings() {
Set<Setting<?>> settings = new HashSet<>(); Set<Setting<?>> settings = new HashSet<>();
settings.addAll(LdapLoadBalancing.getSettings()); settings.addAll(LdapLoadBalancing.getSettings());
settings.add(Setting.listSetting(URLS_SETTING, Collections.emptyList(), Function.identity(), Setting.Property.NodeScope)); settings.add(Setting.listSetting(URLS_SETTING, Collections.emptyList(), Function.identity(),
settings.add(Setting.timeSetting(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope)); Setting.Property.NodeScope));
settings.add(Setting.timeSetting(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope)); settings.add(Setting.timeSetting(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT,
settings.add(Setting.timeSetting(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT, Setting.Property.NodeScope)); Setting.Property.NodeScope));
settings.add(Setting.boolSetting(HOSTNAME_VERIFICATION_SETTING, true, Setting.Property.NodeScope)); settings.add(Setting.timeSetting(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT,
settings.add(Setting.boolSetting(FOLLOW_REFERRALS_SETTING, true, Setting.Property.NodeScope)); 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()); settings.addAll(SSLConfigurationSettings.withPrefix("ssl.").getAllSettings());
return settings; return settings;
} }
@ -208,7 +244,8 @@ public abstract class SessionFactory {
addresses[i] = url.getHost(); addresses[i] = url.getHost();
ports[i] = url.getPort(); ports[i] = url.getPort();
} catch (LDAPException e) { } 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; return true;
} }
final boolean allSecure = Arrays.stream(ldapUrls).allMatch(s -> STARTS_WITH_LDAPS.matcher(s).find()); final boolean allSecure = Arrays.stream(ldapUrls)
final boolean allClear = Arrays.stream(ldapUrls).allMatch(s -> STARTS_WITH_LDAP.matcher(s).find()); .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) { if (!allSecure && !allClear) {
//No mixing is allowed because we use the same socketfactory //No mixing is allowed because we use the same socketfactory
throw new IllegalArgumentException("configured LDAP protocols are not all equal (ldaps://.. and ldap://..): [" + throw new IllegalArgumentException(
Strings.arrayToCommaDelimitedString(ldapUrls) + "]"); "configured LDAP protocols are not all equal (ldaps://.. and ldap://..): ["
+ Strings.arrayToCommaDelimitedString(ldapUrls) + "]");
} }
return allSecure; return allSecure;

View File

@ -24,15 +24,17 @@ import static org.hamcrest.Matchers.is;
@Network @Network
public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase { 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 { public void testResolveSubTree() throws Exception {
Settings settings = Settings.builder() Settings settings = Settings.builder()
.put("scope", LdapSearchScope.SUB_TREE) .put("scope", LdapSearchScope.SUB_TREE)
.build(); .build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com"); ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
NoOpLogger.INSTANCE, null); List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
containsString("Avengers"), containsString("Avengers"),
containsString("SHIELD"), containsString("SHIELD"),
@ -48,9 +50,10 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
.put("scope", LdapSearchScope.ONE_LEVEL) .put("scope", LdapSearchScope.ONE_LEVEL)
.put("base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com") .put("base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com")
.build(); .build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com"); ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
NoOpLogger.INSTANCE, null); List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
assertThat(groups, hasItem(containsString("Users"))); assertThat(groups, hasItem(containsString("Users")));
} }
@ -59,9 +62,10 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
.put("scope", LdapSearchScope.BASE) .put("scope", LdapSearchScope.BASE)
.put("base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com") .put("base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com")
.build(); .build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com"); ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings,
List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), "DC=ad,DC=test,DC=elasticsearch,DC=com", false);
NoOpLogger.INSTANCE, null); List<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
assertThat(groups, hasItem(containsString("CN=Users,CN=Builtin"))); 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"; final String dn = "CN=Jarvis, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com";
PlainActionFuture<Filter> future = new PlainActionFuture<>(); 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(); Filter query = future.actionGet();
assertValidSidQuery(query, expectedSids); assertValidSidQuery(query, expectedSids);
} }
@ -87,7 +92,8 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
"S-1-5-21-3510024162-210737641-214529065-1117"}; //Gods group "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"; final String dn = "CN=Odin, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com";
PlainActionFuture<Filter> future = new PlainActionFuture<>(); 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(); Filter query = future.actionGet();
assertValidSidQuery(query, expectedSids); assertValidSidQuery(query, expectedSids);
} }
@ -105,7 +111,8 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase {
final String dn = BRUCE_BANNER_DN; final String dn = BRUCE_BANNER_DN;
PlainActionFuture<Filter> future = new PlainActionFuture<>(); 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(); Filter query = future.actionGet();
assertValidSidQuery(query, expectedSids); assertValidSidQuery(query, expectedSids);
} }

View File

@ -6,6 +6,7 @@
package org.elasticsearch.xpack.security.authc.ldap; package org.elasticsearch.xpack.security.authc.ldap;
import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException; import org.elasticsearch.common.util.concurrent.UncategorizedExecutionException;
@ -32,6 +33,8 @@ import static org.hamcrest.Matchers.is;
@Network @Network
public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryIntegTests { public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryIntegTests {
private final SecuredString SECURED_PASSWORD = SecuredStringTests.build(PASSWORD);
@Override @Override
public boolean enableWarningsCheck() { public boolean enableWarningsCheck() {
return false; return false;
@ -39,11 +42,14 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testAdAuth() throws Exception { public void testAdAuth() throws Exception {
RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false), globalSettings); RealmConfig config = new RealmConfig("ad-test",
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false),
globalSettings);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config,
sslService);
String userName = "ironman"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
containsString("Geniuses"), containsString("Geniuses"),
@ -64,7 +70,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
String userName = "ades\\ironman"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
containsString("Geniuses"), containsString("Geniuses"),
@ -91,7 +97,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
PlainActionFuture<List<String>> groups = new PlainActionFuture<>(); 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); LDAPException expected = expectThrows(LDAPException.class, groups::actionGet);
assertThat(expected.getMessage(), containsString("A client-side timeout was encountered while waiting")); 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", }; String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", };
for(String user: users) { 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"))); 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); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
String userName = "hulk"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
@ -138,7 +144,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
String userName = "hulk"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
@ -164,7 +170,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
String userName = "hulk"; 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); List<String> groups = groups(ldap);
assertThat(groups, hasItem(containsString("Avengers"))); assertThat(groups, hasItem(containsString("Avengers")));
@ -180,7 +186,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
//Login with the UserPrincipalName //Login with the UserPrincipalName
String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; 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); List<String> groups = groups(ldap);
assertThat(ldap.userDn(), is(userDN)); assertThat(ldap.userDn(), is(userDN));
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
@ -198,7 +204,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
//login with sAMAccountName //login with sAMAccountName
String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; 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)); assertThat(ldap.userDn(), is(userDN));
List<String> groups = groups(ldap); List<String> groups = groups(ldap);
@ -221,7 +227,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService);
//Login with the UserPrincipalName //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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
containsString("CN=Geniuses"), containsString("CN=Geniuses"),
@ -235,7 +241,13 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
public void testStandardLdapConnection() throws Exception { public void testStandardLdapConnection() throws Exception {
String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com"; String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
String userTemplate = "CN={0},CN=Users,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) { if (useGlobalSSL == false) {
settings = Settings.builder() settings = Settings.builder()
.put(settings) .put(settings)
@ -247,7 +259,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService);
String user = "Bruce Banner"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( 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") @SuppressWarnings("unchecked")
public void testStandardLdapWithAttributeGroups() throws Exception { public void testStandardLdapWithAttributeGroups() throws Exception {
String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; 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); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService);
String user = "Bruce Banner"; 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); List<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder( assertThat(groups, containsInAnyOrder(
@ -290,7 +338,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
String userName = "ironman"; String userName = "ironman";
UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class, 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(), instanceOf(ExecutionException.class));
assertThat(e.getCause().getCause(), instanceOf(LDAPException.class)); assertThat(e.getCause().getCause(), instanceOf(LDAPException.class));
final LDAPException expected = (LDAPException) e.getCause().getCause(); final LDAPException expected = (LDAPException) e.getCause().getCause();
@ -309,7 +357,7 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI
String user = "Bruce Banner"; String user = "Bruce Banner";
UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class, 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(), instanceOf(ExecutionException.class));
assertThat(e.getCause().getCause(), instanceOf(LDAPException.class)); assertThat(e.getCause().getCause(), instanceOf(LDAPException.class));
final LDAPException expected = (LDAPException) e.getCause().getCause(); 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.LdapSearchScope;
import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; 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.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.SecuredString;
import org.elasticsearch.xpack.security.authc.support.SecuredStringTests; import org.elasticsearch.xpack.security.authc.support.SecuredStringTests;
import org.elasticsearch.xpack.ssl.SSLService; 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 groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
String userSearchBase = "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; String userSearchBase = "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
Settings settings = Settings.builder() Settings settings = Settings.builder()
.put(LdapTestCase.buildLdapSettings(new String[] { ActiveDirectorySessionFactoryTests.AD_LDAP_URL }, Strings.EMPTY_ARRAY, .put(LdapTestCase.buildLdapSettings(
groupSearchBase, LdapSearchScope.SUB_TREE)) new String[] { ActiveDirectorySessionFactoryTests.AD_LDAP_URL },
Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE, null,
true))
.put("user_search.base_dn", userSearchBase) .put("user_search.base_dn", userSearchBase)
.put("bind_dn", "ironman@ad.test.elasticsearch.com") .put("bind_dn", "ironman@ad.test.elasticsearch.com")
.put("bind_password", ActiveDirectorySessionFactoryTests.PASSWORD) .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 org.junit.BeforeClass;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -92,17 +91,27 @@ public abstract class LdapTestCase extends ESTestCase {
return buildLdapSettings(ldapUrl, userTemplate, groupSearchBase, scope, null); 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) { 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() Settings.Builder builder = Settings.builder()
.putArray(URLS_SETTING, ldapUrl) .putArray(URLS_SETTING, ldapUrl)
.putArray(USER_DN_TEMPLATES_SETTING_KEY, userTemplate) .putArray(USER_DN_TEMPLATES_SETTING_KEY, userTemplate)
.put(SessionFactory.IGNORE_REFERRAL_ERRORS_SETTING.getKey(), ignoreReferralErrors)
.put("group_search.base_dn", groupSearchBase) .put("group_search.base_dn", groupSearchBase)
.put("group_search.scope", scope) .put("group_search.scope", scope)
.put("ssl.verification_mode", VerificationMode.CERTIFICATE); .put("ssl.verification_mode", VerificationMode.CERTIFICATE);
if (serverSetType != null) { if (serverSetType != null) {
builder.put(LdapLoadBalancing.LOAD_BALANCE_SETTINGS + "." + LdapLoadBalancing.LOAD_BALANCE_TYPE_SETTING, builder.put(LdapLoadBalancing.LOAD_BALANCE_SETTINGS + "." +
serverSetType.toString()); LdapLoadBalancing.LOAD_BALANCE_TYPE_SETTING, serverSetType.toString());
} }
return builder.build(); return builder.build();
} }