diff --git a/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryGroupsResolver.java b/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryGroupsResolver.java index cc53f180ea0..a4632e33726 100644 --- a/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryGroupsResolver.java +++ b/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectoryGroupsResolver.java @@ -35,7 +35,7 @@ public class ActiveDirectoryGroupsResolver implements GroupsResolver { this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE); } - public List resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) { + public List resolve(LDAPInterface connection, String userDn, TimeValue timeout, ESLogger logger) { Filter groupSearchFilter = buildGroupQuery(connection, userDn, timeout, logger); logger.debug("group SID to DN search filter: [{}]", groupSearchFilter); @@ -59,7 +59,7 @@ public class ActiveDirectoryGroupsResolver implements GroupsResolver { return groupList; } - static Filter buildGroupQuery(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) { + static Filter buildGroupQuery(LDAPInterface connection, String userDn, TimeValue timeout, ESLogger logger) { try { SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, "tokenGroups"); request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds())); diff --git a/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactory.java b/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactory.java index dce525e8130..6e18a684125 100644 --- a/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactory.java @@ -87,7 +87,7 @@ public class ActiveDirectorySessionFactory extends SessionFactory { * @return An authenticated */ @Override - public LdapSession open(String userName, SecuredString password) { + public LdapSession session(String userName, SecuredString password) { LDAPConnection connection; try { diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java index dc6306f7150..301a0510ac3 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java @@ -6,10 +6,13 @@ package org.elasticsearch.shield.authc.ldap; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestController; +import org.elasticsearch.shield.ShieldSettingsException; import org.elasticsearch.shield.authc.RealmConfig; import org.elasticsearch.shield.authc.ldap.support.AbstractLdapRealm; import org.elasticsearch.shield.authc.ldap.support.GroupToRoleMapper; +import org.elasticsearch.shield.authc.ldap.support.SessionFactory; import org.elasticsearch.shield.ssl.ClientSSLService; import org.elasticsearch.watcher.ResourceWatcherService; @@ -20,7 +23,7 @@ public class LdapRealm extends AbstractLdapRealm { public static final String TYPE = "ldap"; - public LdapRealm(RealmConfig config, LdapSessionFactory ldap, GroupToRoleMapper roleMapper) { + public LdapRealm(RealmConfig config, SessionFactory ldap, GroupToRoleMapper roleMapper) { super(TYPE, config, ldap, roleMapper); } @@ -38,9 +41,21 @@ public class LdapRealm extends AbstractLdapRealm { @Override public LdapRealm create(RealmConfig config) { - LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); + SessionFactory sessionFactory = sessionFactory(config, clientSSLService); GroupToRoleMapper roleMapper = new GroupToRoleMapper(TYPE, config, watcherService, null); return new LdapRealm(config, sessionFactory, roleMapper); } + + static SessionFactory sessionFactory(RealmConfig config, ClientSSLService clientSSLService) { + Settings searchSettings = config.settings().getAsSettings("user_search"); + if (!searchSettings.names().isEmpty()) { + if (config.settings().getAsArray(LdapSessionFactory.USER_DN_TEMPLATES_SETTING).length > 0) { + throw new ShieldSettingsException("settings were found for both user search and user template modes of operation. Please remove the settings for the\n" + + "mode you do not wish to use. For more details refer to the ldap authentication section of the Shield guide."); + } + return new LdapUserSearchSessionFactory(config, clientSSLService); + } + return new LdapSessionFactory(config, clientSSLService); + } } } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactory.java index 1d164dd36c4..6c1d510aa79 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactory.java @@ -77,7 +77,7 @@ public class LdapSessionFactory extends SessionFactory { * @return authenticated exception */ @Override - public LdapSession open(String username, SecuredString password) { + public LdapSession session(String username, SecuredString password) { LDAPConnection connection; try { @@ -115,7 +115,7 @@ public class LdapSessionFactory extends SessionFactory { return MessageFormat.format(template, escapedUsername); } - static LdapSession.GroupsResolver groupResolver(Settings settings) { + static GroupsResolver groupResolver(Settings settings) { Settings searchSettings = settings.getAsSettings("group_search"); if (!searchSettings.names().isEmpty()) { return new SearchGroupsResolver(searchSettings); diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactory.java new file mode 100644 index 00000000000..5dd62db77a5 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactory.java @@ -0,0 +1,167 @@ +/* + * 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.shield.authc.ldap; + +import com.unboundid.ldap.sdk.*; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.ESLogger; +import org.elasticsearch.common.primitives.Ints; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.shield.ShieldSettingsException; +import org.elasticsearch.shield.authc.RealmConfig; +import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope; +import org.elasticsearch.shield.authc.ldap.support.LdapSession; +import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver; +import org.elasticsearch.shield.authc.ldap.support.SessionFactory; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.ssl.ClientSSLService; + +import javax.net.SocketFactory; + +import java.util.Locale; + +import static com.unboundid.ldap.sdk.Filter.createEqualityFilter; +import static com.unboundid.ldap.sdk.Filter.encodeValue; +import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.searchForEntry; + +public class LdapUserSearchSessionFactory extends SessionFactory { + + static final int DEFAULT_CONNECTION_POOL_SIZE = 20; + static final int DEFAULT_CONNECTION_POOL_INITIAL_SIZE = 5; + static final String DEFAULT_USERNAME_ATTRIBUTE = "uid"; + static final TimeValue DEFAULT_HEALTH_CHECK_INTERVAL = TimeValue.timeValueSeconds(60L); + + private final GroupsResolver groupResolver; + private final LDAPConnectionPool connectionPool; + private final String userSearchBaseDn; + private final LdapSearchScope scope; + private final String userAttribute; + private final ServerSet serverSet; + + public LdapUserSearchSessionFactory(RealmConfig config, ClientSSLService sslService) { + super(config); + Settings settings = config.settings(); + userSearchBaseDn = settings.get("user_search.base_dn"); + if (userSearchBaseDn == null) { + throw new ShieldSettingsException("user_search base_dn must be specified"); + } + scope = LdapSearchScope.resolve(settings.get("user_search.scope"), LdapSearchScope.SUB_TREE); + userAttribute = settings.get("user_search.attribute", DEFAULT_USERNAME_ATTRIBUTE); + serverSet = serverSet(settings, sslService); + connectionPool = connectionPool(config.settings(), serverSet, timeout); + groupResolver = groupResolver(settings); + } + + static LDAPConnectionPool connectionPool(Settings settings, ServerSet serverSet, TimeValue timeout) { + SimpleBindRequest bindRequest = bindRequest(settings); + int initialSize = settings.getAsInt("user_search.pool.initial_size", DEFAULT_CONNECTION_POOL_INITIAL_SIZE); + int size = settings.getAsInt("user_search.pool.size", DEFAULT_CONNECTION_POOL_SIZE); + try { + LDAPConnectionPool pool = new LDAPConnectionPool(serverSet, bindRequest, initialSize, size); + pool.setRetryFailedOperationsDueToInvalidConnections(true); + if (settings.getAsBoolean("user_search.pool.health_check.enabled", true)) { + String entryDn = settings.get("user_search.pool.health_check.dn", (bindRequest == null) ? null : bindRequest.getBindDN()); + if (entryDn == null) { + pool.close(); + throw new ShieldSettingsException("[user_search.bind_dn] has not been specified so a value must be specified for [user_search.pool.health_check.dn] or [user_search.pool.health_check.enabled] must be set to false"); + } + long healthCheckInterval = settings.getAsTime("user_search.pool.health_check.interval", DEFAULT_HEALTH_CHECK_INTERVAL).millis(); + // Checks the status of the LDAP connection at a specified interval in the background. We do not check on + // on create as the LDAP server may require authentication to get an entry. We do not check on checkout + // as we always set retry operations and the pool will handle a bad connection without the added latency on every operation + GetEntryLDAPConnectionPoolHealthCheck healthCheck = new GetEntryLDAPConnectionPoolHealthCheck(entryDn, timeout.millis(), false, false, false, true, false); + pool.setHealthCheck(healthCheck); + pool.setHealthCheckIntervalMillis(healthCheckInterval); + } + return pool; + } catch (LDAPException e) { + throw new ShieldLdapException("unable to connect to any LDAP servers", e); + } + } + + static SimpleBindRequest bindRequest(Settings settings) { + SimpleBindRequest request = null; + String bindDn = settings.get("user_search.bind_dn"); + if (bindDn != null) { + request = new SimpleBindRequest(bindDn, settings.get("user_search.bind_password")); + } + return request; + } + + ServerSet serverSet(Settings settings, ClientSSLService clientSSLService) { + // Parse LDAP urls + String[] ldapUrls = settings.getAsArray(URLS_SETTING); + if (ldapUrls == null || ldapUrls.length == 0) { + throw new ShieldSettingsException("missing required LDAP setting [" + URLS_SETTING + "]"); + } + LDAPServers servers = new LDAPServers(ldapUrls); + LDAPConnectionOptions options = connectionOptions(settings); + SocketFactory socketFactory; + if (servers.ssl()) { + socketFactory = clientSSLService.sslSocketFactory(); + if (settings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) { + logger.debug("using encryption for LDAP connections with hostname verification"); + } else { + logger.debug("using encryption for LDAP connections without hostname verification"); + } + } else { + socketFactory = null; + } + FailoverServerSet serverSet = new FailoverServerSet(servers.addresses(), servers.ports(), socketFactory, options); + serverSet.setReOrderOnFailover(true); + return serverSet; + } + + @Override + public LdapSession session(String user, SecuredString password) { + SearchRequest request = new SearchRequest(userSearchBaseDn, scope.scope(), createEqualityFilter(userAttribute, encodeValue(user)), Strings.EMPTY_ARRAY); + request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds())); + try { + SearchResultEntry entry = searchForEntry(connectionPool, request, logger); + if (entry == null) { + throw new ShieldLdapException("failed to find user [" + user + "] with search base [" + userSearchBaseDn + "] scope [" + scope.toString().toLowerCase(Locale.ENGLISH) +"]"); + } + String dn = entry.getDN(); + tryBind(dn, password); + return new LdapSession(logger, connectionPool, dn, groupResolver, timeout); + } catch (LDAPException e) { + throw new ShieldLdapException("failed to authenticate user [" + user + "]", e); + } + } + + private void tryBind(String dn, SecuredString password) { + LDAPConnection bindConnection; + try { + bindConnection = serverSet.getConnection(); + } catch (LDAPException e) { + throw new ShieldLdapException("unable to connect to any LDAP servers for bind", e); + } + + try { + bindConnection.bind(dn, new String(password.internalChars())); + } catch (LDAPException e) { + throw new ShieldLdapException("failed LDAP authentication", dn, e); + } finally { + bindConnection.close(); + } + } + + /* + * This method is used to cleanup the connections for tests + */ + void shutdown() { + connectionPool.close(); + } + + static GroupsResolver groupResolver(Settings settings) { + Settings searchSettings = settings.getAsSettings("group_search"); + if (!searchSettings.names().isEmpty()) { + return new SearchGroupsResolver(searchSettings); + } + return new UserAttributeGroupsResolver(settings); + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java b/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java index 58e75c765a1..5ebbc67ea92 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/SearchGroupsResolver.java @@ -45,7 +45,7 @@ class SearchGroupsResolver implements GroupsResolver { } @Override - public List resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) { + public List resolve(LDAPInterface connection, String userDn, TimeValue timeout, ESLogger logger) { List groups = new LinkedList<>(); String userId = userAttribute != null ? readUserAttribute(connection, userDn, timeout, logger) : userDn; @@ -63,7 +63,7 @@ class SearchGroupsResolver implements GroupsResolver { return groups; } - String readUserAttribute(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) { + String readUserAttribute(LDAPInterface connection, String userDn, TimeValue timeout, ESLogger logger) { try { SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, userAttribute); request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds())); diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java b/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java index ba32992da97..65a2c207e6d 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/UserAttributeGroupsResolver.java @@ -35,7 +35,7 @@ class UserAttributeGroupsResolver implements GroupsResolver { } @Override - public List resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) { + public List resolve(LDAPInterface connection, String userDn, TimeValue timeout, ESLogger logger) { try { SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, attribute); request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds())); diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java b/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java index 9f41ae98c41..db9d0f4b2f5 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/support/AbstractLdapRealm.java @@ -39,7 +39,7 @@ public abstract class AbstractLdapRealm extends CachingUsernamePasswordRealm { */ @Override protected User doAuthenticate(UsernamePasswordToken token) { - try (LdapSession session = sessionFactory.open(token.principal(), token.credentials())) { + try (LdapSession session = sessionFactory.session(token.principal(), token.credentials())) { List groupDNs = session.groups(); Set roles = roleMapper.mapRoles(groupDNs); return new User.Simple(token.principal(), roles.toArray(new String[roles.size()])); diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapSession.java b/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapSession.java index a63ee330571..5216ed30261 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapSession.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapSession.java @@ -6,6 +6,7 @@ package org.elasticsearch.shield.authc.ldap.support; import com.unboundid.ldap.sdk.LDAPConnection; +import com.unboundid.ldap.sdk.LDAPInterface; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.unit.TimeValue; @@ -18,7 +19,7 @@ import java.util.List; public class LdapSession implements Closeable { protected final ESLogger logger; - protected final LDAPConnection ldapConnection; + protected final LDAPInterface ldap; protected final String bindDn; protected final GroupsResolver groupsResolver; protected final TimeValue timeout; @@ -31,9 +32,9 @@ public class LdapSession implements Closeable { * outside of and be reused across all connections. We can't keep a static logger in this class * since we want the logger to be contextual (i.e. aware of the settings and its environment). */ - public LdapSession(ESLogger logger, LDAPConnection connection, String boundName, GroupsResolver groupsResolver, TimeValue timeout) { + public LdapSession(ESLogger logger, LDAPInterface connection, String boundName, GroupsResolver groupsResolver, TimeValue timeout) { this.logger = logger; - this.ldapConnection = connection; + this.ldap = connection; this.bindDn = boundName; this.groupsResolver = groupsResolver; this.timeout = timeout; @@ -44,7 +45,10 @@ public class LdapSession implements Closeable { */ @Override public void close() { - ldapConnection.close(); + // Only if it is an LDAPConnection do we need to close it + if (ldap instanceof LDAPConnection) { + ((LDAPConnection) ldap).close(); + } } /** @@ -58,12 +62,12 @@ public class LdapSession implements Closeable { * @return List of fully distinguished group names */ public List groups() { - return groupsResolver.resolve(ldapConnection, bindDn, timeout, logger); + return groupsResolver.resolve(ldap, bindDn, timeout, logger); } public static interface GroupsResolver { - List resolve(LDAPConnection ldapConnection, String userDn, TimeValue timeout, ESLogger logger); + List resolve(LDAPInterface ldapConnection, String userDn, TimeValue timeout, ESLogger logger); } } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapUtils.java b/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapUtils.java index c91391d959a..c74737a580e 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapUtils.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/support/LdapUtils.java @@ -39,16 +39,16 @@ public final class LdapUtils { /** * This method performs a LDAPConnection.search(...) operation while handling referral exceptions. This is necessary * to maintain backwards compatibility - * @param ldapConnection + * @param ldap * @param searchRequest * @param logger * @return * @throws LDAPException */ - public static SearchResult search(LDAPConnection ldapConnection, SearchRequest searchRequest, ESLogger logger) throws LDAPException { + public static SearchResult search(LDAPInterface ldap, SearchRequest searchRequest, ESLogger logger) throws LDAPException { SearchResult results; try { - results = ldapConnection.search(searchRequest); + results = ldap.search(searchRequest); } catch (LDAPSearchException e) { if (e.getResultCode().equals(ResultCode.REFERRAL) && e.getSearchResult() != null) { if (logger.isDebugEnabled()){ @@ -65,16 +65,16 @@ public final class LdapUtils { /** * This method performs a LDAPConnection.searchForEntry(...) operation while handling referral exceptions. This is necessary * to maintain backwards compatibility - * @param ldapConnection + * @param ldap * @param searchRequest * @param logger * @return * @throws LDAPException */ - public static SearchResultEntry searchForEntry(LDAPConnection ldapConnection, SearchRequest searchRequest, ESLogger logger) throws LDAPException { + public static SearchResultEntry searchForEntry(LDAPInterface ldap, SearchRequest searchRequest, ESLogger logger) throws LDAPException { SearchResultEntry entry; try { - entry = ldapConnection.searchForEntry(searchRequest); + entry = ldap.searchForEntry(searchRequest); } catch (LDAPSearchException e) { if (e.getResultCode().equals(ResultCode.REFERRAL) && e.getSearchResult() != null && e.getSearchResult().getEntryCount() > 0) { if (logger.isDebugEnabled()){ diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/support/SessionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/support/SessionFactory.java index 5cd2f8fd2eb..d74ac5837b5 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/support/SessionFactory.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/support/SessionFactory.java @@ -33,7 +33,7 @@ import static org.elasticsearch.common.collect.Iterables.all; * A standard looking usage pattern could look like this:
     ConnectionFactory factory = ...
-    try (LdapConnection session = factory.open(...)) {
+    try (LdapConnection session = factory.session(...)) {
         ...do stuff with the session
     }
  
@@ -74,7 +74,7 @@ public abstract class SessionFactory { * @param user The name of the user to authenticate the connection with. * @param password The password of the user */ - public abstract LdapSession open(String user, SecuredString password); + public abstract LdapSession session(String user, SecuredString password); protected static LDAPConnectionOptions connectionOptions(Settings settings) { LDAPConnectionOptions options = new LDAPConnectionOptions(); diff --git a/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactoryTests.java index 801aba5543c..dd4d9520775 100644 --- a/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactoryTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/activedirectory/ActiveDirectorySessionFactoryTests.java @@ -59,7 +59,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); String userName = "ironman"; - try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(userName, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( containsString("Geniuses"), @@ -85,7 +85,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { RealmConfig config = new RealmConfig("ad-test", settings); ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); - try (LdapSession ldap = sessionFactory.open("ironman", SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session("ironman", SecuredStringTests.build(PASSWORD))) { // In certain cases we may have a successful bind, but a search should take longer and cause a timeout ldap.groups(); fail("The TCP connection should timeout before getting groups back"); @@ -101,7 +101,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", }; for(String user: users) { - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { assertThat("group avenger test for user "+user, ldap.groups(), hasItem(Matchers.containsString("Avengers"))); } } @@ -114,7 +114,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); String userName = "hulk"; - try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(userName, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( @@ -135,7 +135,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); String userName = "hulk"; - try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(userName, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( @@ -160,7 +160,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); String userName = "hulk"; - try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(userName, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, hasItem(containsString("Avengers"))); @@ -175,7 +175,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { //Login with the UserPrincipalName String userDN; - try (LdapSession ldap = sessionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session("erik.selvig", SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); userDN = ldap.authenticatedUserDn(); assertThat(groups, containsInAnyOrder( @@ -184,7 +184,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { containsString("Domain Users"))); } //Same user but login with sAMAccountName - try (LdapSession ldap = sessionFactory.open("selvig", SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session("selvig", SecuredStringTests.build(PASSWORD))) { assertThat(ldap.authenticatedUserDn(), is(userDN)); List groups = ldap.groups(); @@ -205,7 +205,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); //Login with the UserPrincipalName - try (LdapSession ldap = sessionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session("erik.selvig", SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( containsString("Geniuses"), @@ -224,7 +224,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); String user = "Bruce Banner"; - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( @@ -243,7 +243,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); String user = "Bruce Banner"; - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder( @@ -260,7 +260,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService); String userName = "ironman"; - try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(userName, SecuredStringTests.build(PASSWORD))) { fail("Test active directory certificate does not have proper hostname/ip address for hostname verification"); } catch (ActiveDirectoryException e) { assertThat(e.getMessage(), containsString("failed to connect to any active directory servers")); @@ -279,7 +279,7 @@ public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase { LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); String user = "Bruce Banner"; - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { fail("Test active directory certificate does not have proper hostname/ip address for hostname verification"); } } diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java index 33d61df9142..619a8e99279 100644 --- a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java @@ -8,8 +8,10 @@ package org.elasticsearch.shield.authc.ldap; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestController; +import org.elasticsearch.shield.ShieldSettingsException; import org.elasticsearch.shield.User; import org.elasticsearch.shield.authc.RealmConfig; +import org.elasticsearch.shield.authc.ldap.support.SessionFactory; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.shield.authc.support.SecuredStringTests; import org.elasticsearch.shield.authc.support.UsernamePasswordToken; @@ -22,8 +24,10 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; -import static org.hamcrest.Matchers.arrayContaining; -import static org.hamcrest.Matchers.notNullValue; +import static org.elasticsearch.shield.authc.ldap.LdapSessionFactory.USER_DN_TEMPLATES_SETTING; +import static org.elasticsearch.shield.authc.ldap.support.SessionFactory.HOSTNAME_VERIFICATION_SETTING; +import static org.elasticsearch.shield.authc.ldap.support.SessionFactory.URLS_SETTING; +import static org.hamcrest.Matchers.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.*; @@ -103,8 +107,8 @@ public class LdapRealmTest extends LdapTest { ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); - //verify one and only one open -> caching is working - verify(ldapFactory, times(1)).open(anyString(), any(SecuredString.class)); + //verify one and only one session -> caching is working + verify(ldapFactory, times(1)).session(anyString(), any(SecuredString.class)); } @Test @@ -123,15 +127,15 @@ public class LdapRealmTest extends LdapTest { ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); - //verify one and only one open -> caching is working - verify(ldapFactory, times(1)).open(anyString(), any(SecuredString.class)); + //verify one and only one session -> caching is working + verify(ldapFactory, times(1)).session(anyString(), any(SecuredString.class)); roleMapper.notifyRefresh(); ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); - //we need to open again - verify(ldapFactory, times(2)).open(anyString(), any(SecuredString.class)); + //we need to session again + verify(ldapFactory, times(2)).session(anyString(), any(SecuredString.class)); } @Test @@ -151,7 +155,62 @@ public class LdapRealmTest extends LdapTest { ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD))); //verify two and only two binds -> caching is disabled - verify(ldapFactory, times(2)).open(anyString(), any(SecuredString.class)); + verify(ldapFactory, times(2)).session(anyString(), any(SecuredString.class)); } + @Test + public void testLdapRealmSelectsLdapSessionFactory() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userTemplate = VALID_USER_TEMPLATE; + Settings settings = ImmutableSettings.builder() + .putArray(URLS_SETTING, ldapUrl()) + .putArray(USER_DN_TEMPLATES_SETTING, userTemplate) + .put("group_search.base_dn", groupSearchBase) + .put("group_search.scope", LdapSearchScope.SUB_TREE) + .put(HOSTNAME_VERIFICATION_SETTING, false) + .build(); + RealmConfig config = new RealmConfig("test-ldap-realm", settings); + SessionFactory sessionFactory = LdapRealm.Factory.sessionFactory(config, null); + assertThat(sessionFactory, is(instanceOf(LdapSessionFactory.class))); + } + + @Test + public void testLdapRealmSelectsLdapUserSearchSessionFactory() throws Exception { + String groupSearchBase = "o=sevenSeas"; + Settings settings = ImmutableSettings.builder() + .putArray(URLS_SETTING, ldapUrl()) + .put("user_search.base_dn", "") + .put("user_search.bind_dn", "cn=Thomas Masterman Hardy,ou=people,o=sevenSeas") + .put("user_search.bind_password", PASSWORD) + .put("group_search.base_dn", groupSearchBase) + .put("group_search.scope", LdapSearchScope.SUB_TREE) + .put(HOSTNAME_VERIFICATION_SETTING, false) + .build(); + RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings); + SessionFactory sessionFactory = LdapRealm.Factory.sessionFactory(config, null); + try { + assertThat(sessionFactory, is(instanceOf(LdapUserSearchSessionFactory.class))); + } finally { + ((LdapUserSearchSessionFactory)sessionFactory).shutdown(); + } + } + + @Test + public void testLdapRealmThrowsExceptionForUserTemplateAndSearchSettings() throws Exception { + Settings settings = ImmutableSettings.builder() + .putArray(URLS_SETTING, ldapUrl()) + .putArray(USER_DN_TEMPLATES_SETTING, "cn=foo") + .put("user_search.base_dn", "cn=bar") + .put("group_search.base_dn", "") + .put("group_search.scope", LdapSearchScope.SUB_TREE) + .put(HOSTNAME_VERIFICATION_SETTING, false) + .build(); + RealmConfig config = new RealmConfig("test-ldap-realm-user-search", settings); + try { + LdapRealm.Factory.sessionFactory(config, null); + fail("an exception should have been thrown because both user template and user search settings were specified"); + } catch (ShieldSettingsException e) { + assertThat(e.getMessage(), containsString("settings were found for both user search and user template")); + } + } } diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactoryTests.java index 68e14ccfe79..072dcdecf9d 100644 --- a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactoryTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapSessionFactoryTests.java @@ -42,7 +42,7 @@ public class LdapSessionFactoryTests extends LdapTest { ldapServer.setProcessingDelayMillis(500L); long start = System.currentTimeMillis(); - try (LdapSession session = sessionFactory.open(user, userPass)) { + try (LdapSession session = sessionFactory.session(user, userPass)) { fail("expected connection timeout error here"); } catch (Throwable t) { long time = System.currentTimeMillis() - start; @@ -73,7 +73,7 @@ public class LdapSessionFactoryTests extends LdapTest { SecuredString userPass = SecuredStringTests.build("pass"); long start = System.currentTimeMillis(); - try (LdapSession session = sessionFactory.open(user, userPass)) { + try (LdapSession session = sessionFactory.session(user, userPass)) { fail("expected connection timeout error here"); } catch (Throwable t) { long time = System.currentTimeMillis() - start; @@ -98,7 +98,7 @@ public class LdapSessionFactoryTests extends LdapTest { String user = "Horatio Hornblower"; SecuredString userPass = SecuredStringTests.build("pass"); - try (LdapSession ldap = sessionFactory.open(user, userPass)) { + try (LdapSession ldap = sessionFactory.session(user, userPass)) { String dn = ldap.authenticatedUserDn(); assertThat(dn, containsString(user)); } @@ -119,7 +119,7 @@ public class LdapSessionFactoryTests extends LdapTest { String user = "Horatio Hornblower"; SecuredString userPass = SecuredStringTests.build("pass"); - try (LdapSession ldapConnection = ldapFac.open(user, userPass)) { + try (LdapSession ldapConnection = ldapFac.session(user, userPass)) { } } @@ -134,7 +134,7 @@ public class LdapSessionFactoryTests extends LdapTest { String user = "Horatio Hornblower"; SecuredString userPass = SecuredStringTests.build("pass"); - try (LdapSession ldap = ldapFac.open(user, userPass)) { + try (LdapSession ldap = ldapFac.session(user, userPass)) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); } @@ -149,7 +149,7 @@ public class LdapSessionFactoryTests extends LdapTest { LdapSessionFactory ldapFac = new LdapSessionFactory(config, null); String user = "Horatio Hornblower"; - try (LdapSession ldap = ldapFac.open(user, SecuredStringTests.build("pass"))) { + try (LdapSession ldap = ldapFac.session(user, SecuredStringTests.build("pass"))) { List groups = ldap.groups(); assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); } @@ -166,7 +166,7 @@ public class LdapSessionFactoryTests extends LdapTest { String user = "Horatio Hornblower"; SecuredString userPass = SecuredStringTests.build("pass"); - try (LdapSession ldap = ldapFac.open(user, userPass)) { + try (LdapSession ldap = ldapFac.session(user, userPass)) { List groups = ldap.groups(); assertThat(groups.size(), is(1)); assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas")); diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactoryTests.java new file mode 100644 index 00000000000..594a83bfd75 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapUserSearchSessionFactoryTests.java @@ -0,0 +1,375 @@ +/* + * 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.shield.authc.ldap; + +import com.unboundid.ldap.sdk.*; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.shield.ShieldSettingsException; +import org.elasticsearch.shield.authc.RealmConfig; +import org.elasticsearch.shield.authc.activedirectory.ActiveDirectorySessionFactoryTests; +import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope; +import org.elasticsearch.shield.authc.ldap.support.LdapSession; +import org.elasticsearch.shield.authc.ldap.support.LdapTest; +import org.elasticsearch.shield.authc.support.SecuredString; +import org.elasticsearch.shield.authc.support.SecuredStringTests; +import org.elasticsearch.shield.ssl.ClientSSLService; +import org.junit.Before; +import org.junit.Test; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.List; + +import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder; +import static org.hamcrest.Matchers.*; + +public class LdapUserSearchSessionFactoryTests extends LdapTest { + + private ClientSSLService clientSSLService; + + @Before + public void initializeSslSocketFactory() throws Exception { + Path keystore = Paths.get(getResource("support/ldaptrust.jks").toURI()).toAbsolutePath(); + + /* + * Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext. + * If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname + * verification tests since a re-established connection does not perform hostname verification. + */ + clientSSLService = new ClientSSLService(ImmutableSettings.builder() + .put("shield.ssl.keystore.path", keystore) + .put("shield.ssl.keystore.password", "changeit") + .build()); + } + + @Test + public void testUserSearchSubTree() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.attribute", "cn") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + String dn = ldap.authenticatedUserDn(); + assertThat(dn, containsString(user)); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchBaseScopeFailsWithWrongBaseDN() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.scope", LdapSearchScope.BASE) + .put("user_search.attribute", "cn") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + fail("the user should not have been found"); + } catch (ShieldLdapException e) { + assertThat(e.getMessage(), containsString("failed to find user [William Bush] with search base [o=sevenSeas] scope [base]")); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchBaseScopePassesWithCorrectBaseDN() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "cn=William Bush,ou=people,o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.scope", LdapSearchScope.BASE) + .put("user_search.attribute", "cn") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + String dn = ldap.authenticatedUserDn(); + assertThat(dn, containsString(user)); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchOneLevelScopeFailsWithWrongBaseDN() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.scope", LdapSearchScope.ONE_LEVEL) + .put("user_search.attribute", "cn") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + fail("the user should not have been found"); + } catch (ShieldLdapException e) { + assertThat(e.getMessage(), containsString("failed to find user [William Bush] with search base [o=sevenSeas] scope [one_level]")); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchOneLevelScopePassesWithCorrectBaseDN() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "ou=people,o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.scope", LdapSearchScope.ONE_LEVEL) + .put("user_search.attribute", "cn") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + String dn = ldap.authenticatedUserDn(); + assertThat(dn, containsString(user)); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchWithBadAttributeFails() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.attribute", "uid1") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "William Bush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + fail("the user should not have been found"); + } catch (ShieldLdapException e) { + assertThat(e.getMessage(), containsString("failed to find user [William Bush] with search base [o=sevenSeas] scope [sub_tree]")); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchWithoutAttributePasses() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .build()); + + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, null); + + String user = "wbush"; + SecuredString userPass = SecuredStringTests.build("pass"); + + try (LdapSession ldap = sessionFactory.session(user, userPass)) { + String dn = ldap.authenticatedUserDn(); + assertThat(dn, containsString("William Bush")); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchWithActiveDirectory() { + String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com"; + String userSearchBase = "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; + Settings settings = settingsBuilder() + .put(LdapTest.buildLdapSettings(ActiveDirectorySessionFactoryTests.AD_LDAP_URL, Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "ironman@ad.test.elasticsearch.com") + .put("user_search.bind_password", ActiveDirectorySessionFactoryTests.PASSWORD) + .put("user_search.attribute", "cn") + .build(); + RealmConfig config = new RealmConfig("ad-as-ldap-test", settings); + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, clientSSLService); + + String user = "Bruce Banner"; + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(ActiveDirectorySessionFactoryTests.PASSWORD))) { + List groups = ldap.groups(); + + assertThat(groups, containsInAnyOrder( + containsString("Avengers"), + containsString("SHIELD"), + containsString("Geniuses"), + containsString("Philanthropists"))); + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testUserSearchwithBindUserOpenLDAP() { + String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"; + String userSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"; + RealmConfig config = new RealmConfig("oldap-test", settingsBuilder() + .put(LdapTest.buildLdapSettings(OpenLdapTests.OPEN_LDAP_URL, Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.ONE_LEVEL)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "uid=blackwidow,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com") + .put("user_search.bind_password", OpenLdapTests.PASSWORD) + .build()); + LdapUserSearchSessionFactory sessionFactory = new LdapUserSearchSessionFactory(config, clientSSLService); + + String[] users = new String[] { "cap", "hawkeye", "hulk", "ironman", "thor" }; + try { + for (String user : users) { + LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(OpenLdapTests.PASSWORD)); + assertThat(ldap.authenticatedUserDn(), is(equalTo(MessageFormat.format("uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com", user)))); + assertThat(ldap.groups(), hasItem(containsString("Avengers"))); + ldap.close(); + } + } finally { + sessionFactory.shutdown(); + } + } + + @Test + public void testConnectionPoolDefaultSettings() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .build()); + + LDAPConnectionPool connectionPool = LdapUserSearchSessionFactory.connectionPool(config.settings(), new SingleServerSet("localhost", ldapServer.getListenPort()), TimeValue.timeValueSeconds(5)); + try { + assertThat(connectionPool.getCurrentAvailableConnections(), is(LdapUserSearchSessionFactory.DEFAULT_CONNECTION_POOL_INITIAL_SIZE)); + assertThat(connectionPool.getMaximumAvailableConnections(), is(LdapUserSearchSessionFactory.DEFAULT_CONNECTION_POOL_SIZE)); + assertEquals(connectionPool.getHealthCheck().getClass(), GetEntryLDAPConnectionPoolHealthCheck.class); + GetEntryLDAPConnectionPoolHealthCheck healthCheck = (GetEntryLDAPConnectionPoolHealthCheck) connectionPool.getHealthCheck(); + assertThat(healthCheck.getEntryDN(), is("cn=Horatio Hornblower,ou=people,o=sevenSeas")); + assertThat(healthCheck.getMaxResponseTimeMillis(), is(LdapUserSearchSessionFactory.TIMEOUT_DEFAULT.millis())); + } finally { + connectionPool.close(); + } + } + + @Test + public void testConnectionPoolSettings() throws Exception { + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_dn", "cn=Horatio Hornblower,ou=people,o=sevenSeas") + .put("user_search.bind_password", "pass") + .put("user_search.pool.initial_size", 10) + .put("user_search.pool.size", 12) + .put("user_search.pool.health_check.enabled", false) + .build()); + + LDAPConnectionPool connectionPool = LdapUserSearchSessionFactory.connectionPool(config.settings(), new SingleServerSet("localhost", ldapServer.getListenPort()), TimeValue.timeValueSeconds(5)); + try { + assertThat(connectionPool.getCurrentAvailableConnections(), is(10)); + assertThat(connectionPool.getMaximumAvailableConnections(), is(12)); + assertThat(connectionPool.retryFailedOperationsDueToInvalidConnections(), is(true)); + assertEquals(connectionPool.getHealthCheck().getClass(), LDAPConnectionPoolHealthCheck.class); + } finally { + connectionPool.close(); + } + } + + @Test + public void testThatEmptyBindDNThrowsExceptionWithHealthCheckEnabled() throws Exception{ + String groupSearchBase = "o=sevenSeas"; + String userSearchBase = "o=sevenSeas"; + RealmConfig config = new RealmConfig("ldap_realm", settingsBuilder() + .put(buildLdapSettings(ldapUrl(), Strings.EMPTY_ARRAY, groupSearchBase, LdapSearchScope.SUB_TREE)) + .put("user_search.base_dn", userSearchBase) + .put("user_search.bind_password", "pass") + .build()); + + try { + new LdapUserSearchSessionFactory(config, null); + } catch (ShieldSettingsException e) { + assertThat(e.getMessage(), containsString("[user_search.bind_dn] has not been specified so a value must be specified for [user_search.pool.health_check.dn] or [user_search.pool.health_check.enabled] must be set to false")); + } + } + + @Test + public void testEmptyBindDNReturnsNullBindRequest() { + BindRequest request = LdapUserSearchSessionFactory.bindRequest(settingsBuilder().put("user_search.bind_password", "password").build()); + assertThat(request, is(nullValue())); + } + + @Test + public void testThatBindRequestReturnsSimpleBindRequest() { + BindRequest request = LdapUserSearchSessionFactory.bindRequest(settingsBuilder() + .put("user_search.bind_password", "password") + .put("user_search.bind_dn", "cn=ironman") + .build()); + assertEquals(request.getClass(), SimpleBindRequest.class); + SimpleBindRequest simpleBindRequest = (SimpleBindRequest) request; + assertThat(simpleBindRequest.getBindDN(), is("cn=ironman")); + } +} diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java index 979651f9970..39e0bcbf1cd 100644 --- a/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/OpenLdapTests.java @@ -23,8 +23,7 @@ import org.junit.Test; import java.nio.file.Path; import java.nio.file.Paths; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.*; @Network public class OpenLdapTests extends ElasticsearchTestCase { @@ -59,7 +58,7 @@ public class OpenLdapTests extends ElasticsearchTestCase { String[] users = new String[] { "blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor" }; for (String user : users) { - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { assertThat(ldap.groups(), hasItem(containsString("Avengers"))); } } @@ -76,7 +75,7 @@ public class OpenLdapTests extends ElasticsearchTestCase { String[] users = new String[] { "blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor" }; for (String user : users) { - LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD)); + LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD)); assertThat(ldap.groups(), hasItem(containsString("Avengers"))); ldap.close(); } @@ -94,7 +93,7 @@ public class OpenLdapTests extends ElasticsearchTestCase { RealmConfig config = new RealmConfig("oldap-test", settings); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); - try (LdapSession ldap = sessionFactory.open("selvig", SecuredStringTests.build(PASSWORD))){ + try (LdapSession ldap = sessionFactory.session("selvig", SecuredStringTests.build(PASSWORD))){ assertThat(ldap.groups(), hasItem(containsString("Geniuses"))); } } @@ -112,7 +111,7 @@ public class OpenLdapTests extends ElasticsearchTestCase { RealmConfig config = new RealmConfig("oldap-test", settings); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); - try (LdapSession ldap = sessionFactory.open("thor", SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session("thor", SecuredStringTests.build(PASSWORD))) { // In certain cases we may have a successful bind, but a search should take longer and cause a timeout ldap.groups(); fail("The TCP connection should timeout before getting groups back"); @@ -135,7 +134,7 @@ public class OpenLdapTests extends ElasticsearchTestCase { LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService); String user = "blackwidow"; - try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) { + try (LdapSession ldap = sessionFactory.session(user, SecuredStringTests.build(PASSWORD))) { fail("OpenLDAP certificate does not contain the correct hostname/ip so hostname verification should fail on open"); } catch (ShieldLdapException e) { assertThat(e.getMessage(), containsString("failed to connect to any LDAP servers")); diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/support/SessionFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/support/SessionFactoryTests.java index 867be698dff..b12ea16e052 100644 --- a/src/test/java/org/elasticsearch/shield/authc/ldap/support/SessionFactoryTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/support/SessionFactoryTests.java @@ -52,7 +52,7 @@ public class SessionFactoryTests extends ElasticsearchTestCase { return new SessionFactory(new RealmConfig("_name")) { @Override - public LdapSession open(String user, SecuredString password) { + public LdapSession session(String user, SecuredString password) { return null; } };