diff --git a/docs/en/security/authentication/active-directory-realm.asciidoc b/docs/en/security/authentication/active-directory-realm.asciidoc index 8751c34c91f..17f5bdde90f 100644 --- a/docs/en/security/authentication/active-directory-realm.asciidoc +++ b/docs/en/security/authentication/active-directory-realm.asciidoc @@ -33,6 +33,12 @@ NOTE: When you use Active Directory for authentication, the username entered by the user is expected to match the `sAMAccountName` or `userPrincipalName`, not the common name. +The Active Directory realm authenticates users using an LDAP bind request. After +authenticating the user, the realm then searches to find the user's entry in +Active Directory. Once the user has been found, the Active Directory realm then +retrieves the user's group memberships from the `tokenGroups` attribute on the +user's entry in Active Directory. + To configure an `active_directory` realm: . Add a realm configuration of type `active_directory` to `elasticsearch.yml` @@ -63,13 +69,10 @@ xpack: order: 0 <1> domain_name: ad.example.com url: ldaps://ad.example.com:636 <2> - unmapped_groups_as_roles: true <3> ------------------------------------------------------------ <1> The realm order controls the order in which the configured realms are checked when authenticating a user. <2> If you don't specify the URL, it defaults to `ldap::389`. -<3> When this option is enabled, Active Directory groups are automatically mapped - to roles of the same name. + IMPORTANT: When you configure realms in `elasticsearch.yml`, only the realms you specify are used for authentication. If you also want to use the @@ -77,6 +80,42 @@ realms you specify are used for authentication. If you also want to use the . Restart Elasticsearch. +===== Configuring a Bind User +By default, all of the LDAP operations are run by the user that {security} is +authenticating. In some cases, regular users may not be able to access all of the +necessary items within Active Directory and a _bind user_ is needed. A bind user +can be configured and will be used to perform all operations other than the LDAP +bind request, which is required to authenticate the credentials provided by the user. + +The use of a bind user enables the <> to be +used with the Active Directory realm and the ability to maintain a set of pooled +connections to Active Directory. These pooled connection reduce the number of +resources that must be created and destroyed with every user authentication. + +The following example shows the configuration of a bind user through the user of the +`bind_dn` and `bind_password` settings. + +[source, yaml] +------------------------------------------------------------ +xpack: + security: + authc: + realms: + active_directory: + type: active_directory + order: 0 + domain_name: ad.example.com + url: ldaps://ad.example.com:636 + bind_dn: es_svc_user@ad.example.com <1> + bind_password: es_svc_user_password +------------------------------------------------------------ +<1> This is the user that all Active Directory search requests are executed as. + Without a bind user configured, all requests run as the user that is authenticating + with Elasticsearch. + +When a bind user is configured, connection pooling is enabled by default. +Connection pooling can be disabled using the `user_search.pool.enabled` setting. + ===== Multiple Domain Support When authenticating users across multiple domains in a forest, there are a few minor differences in the configuration and the way that users will authenticate. The `domain_name` @@ -176,6 +215,14 @@ operation are supported: failover and load balancing assuming an unencrypted connection to port 389. For example, `ldap://:389`. This settings is required when connecting using SSL/TLS or via a custom port. +| `bind_dn` | no | The DN of the user that is used to bind to Active Directory + and perform searches. Due to its potential security + impact, `bind_dn` is not exposed via the + {ref}/cluster-nodes-info.html#cluster-nodes-info[nodes info API]. +| `bind_password` | no | The password for the user that is used to bind to + Active Directory. Due to its potential security impact, + `bind_password` is not exposed via the + {ref}/cluster-nodes-info.html#cluster-nodes-info[nodes info API]. | `load_balance.type` | no | The behavior to use when there are multiple LDAP URLs defined. For supported values see <>. | `load_balance.cache_ttl` | no | When using `dns_failover` or `dns_round_robin` as the load @@ -209,6 +256,22 @@ operation are supported: failover and load balancing must be a valid LDAP user search filter, for example `(&(objectClass=user)(sAMAccountName={0}))`. For more information, see https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx[Search Filter Syntax]. +| `user_search.pool.enabled` | no | Enables or disables connection pooling for user search. When + disabled a new connection is created for every search. The + default is `true` when `bind_dn` is provided. +| `user_search.pool.size` | no | Specifies the maximum number of connections to Active Directory + server to allow in the connection pool. Defaults to `20`. +| `user_search.pool.initial_size` | no | The initial number of connections to create to Active Directory + server on startup. Defaults to `0`. Values greater than `0` + could cause startup failures if the LDAP server is down. +| `user_search.pool.health_check.enabled` | no | Enables or disables a health check on Active Directory connections in + the connection pool. Connections are checked in the + background at the specified interval. Defaults to `true`. +| `user_search.pool.health_check.dn` | no | Specifies the distinguished name to retrieve as part of + the health check. Defaults to the value of `bind_dn` if present, and if + not falls back to `user_search.base_dn`. +| `user_search.pool.health_check.interval` | no | How often to perform background checks of connections in + the pool. Defaults to `60s`. | `group_search.base_dn` | no | Specifies the context to search for groups in which the user has membership. Defaults to the root of the Active Directory domain. diff --git a/docs/en/security/authorization/run-as-privilege.asciidoc b/docs/en/security/authorization/run-as-privilege.asciidoc index 0db5b53a9dd..0a391e7804a 100644 --- a/docs/en/security/authorization/run-as-privilege.asciidoc +++ b/docs/en/security/authorization/run-as-privilege.asciidoc @@ -8,10 +8,10 @@ users, you can use the _run as_ mechanism to restrict data access according to To "run as" (impersonate) another user, you must be able to retrieve the user from the realm you use to authenticate. Both the internal `native` and `file` realms -support this out of the box. The LDAP realm however must be configured to run in -_user search_ mode. For more information, see -<>. -The Active Directory and PKI realms do not support "run as". +support this out of the box. The LDAP realm must be configured to run in +<>. The Active Directory realm must be +<> to support _run as_. +The PKI realm does not support _run as_. To submit requests on behalf of other users, you need to have the `run_as` permission. For example, the following role grants permission to submit request diff --git a/docs/en/settings/security-settings.asciidoc b/docs/en/settings/security-settings.asciidoc index 163cc5e98cd..96b3ec288e9 100644 --- a/docs/en/settings/security-settings.asciidoc +++ b/docs/en/settings/security-settings.asciidoc @@ -199,7 +199,7 @@ The attribute to match with the username presented to. Defaults to `uid`. `user_search.pool.enabled`:: Enables or disables connection pooling for user search. When disabled a new connection is created for every search. The -default is `true`. +default is `true` when `bind_dn` is provided. `user_search.pool.size`:: The maximum number of connections to the LDAP server to allow in the @@ -207,7 +207,7 @@ connection pool. Defaults to `20`. `user_search.pool.initial_size`:: The initial number of connections to create to the LDAP server on startup. -Defaults to `5`. +Defaults to `0`. `user_search.pool.health_check.enabled`:: Flag to enable or disable a health check on LDAP connections in the connection @@ -216,12 +216,13 @@ Defaults to `true`. `user_search.pool.health_check.dn`:: The distinguished name to be retrieved as part of the health check. -Defaults to the value of `bind_dn`. Required if `bind_dn` is not -specified. +Defaults to the value of `bind_dn` if present, and if +not falls back to `user_search.base_dn`. `user_search.pool.health_check.interval`:: The interval to perform background checks of connections in the pool. Defaults to `60s`. + `group_search.base_dn`:: The container DN to search for groups in which the user has membership. When this element is absent, Security searches for the attribute specified by @@ -359,6 +360,14 @@ The domain name of Active Directory. The cluster can derive the URL and `user_search_dn` fields from values in this element if those fields are not otherwise specified. Required. +`bind_dn`:: +The DN of the user that will be used to bind to Active Directory and perform searches. +Defaults to Empty. + +`bind_password`:: +The password for the user that will be used to bind to Active Directory. +Defaults to Empty. + `unmapped_groups_as_roles`:: Takes a boolean variable. When this element is set to `true`, the names of any LDAP groups that are not referenced in a role-mapping _file_ are used as role @@ -401,6 +410,32 @@ Specifies a filter to use to lookup a user given a down level logon name must be a valid LDAP user search filter, for example `(&(objectClass=user)(sAMAccountName={0}))`. +`user_search.pool.enabled`:: +Enables or disables connection pooling for user search. When +disabled a new connection is created for every search. The +default is `true` when `bind_dn` is provided. + +`user_search.pool.size`:: +The maximum number of connections to the Active Directory server to allow in the +connection pool. Defaults to `20`. + +`user_search.pool.initial_size`:: +The initial number of connections to create to the Active Directory server on startup. +Defaults to `0`. + +`user_search.pool.health_check.enabled`:: +Flag to enable or disable a health check on Active Directory connections in the connection +pool. Connections are checked in the background at the specified interval. +Defaults to `true`. + +`user_search.pool.health_check.dn`:: +The distinguished name to be retrieved as part of the health check. +Defaults to the value of `bind_dn` if it is a distinguished name. + +`user_search.pool.health_check.interval`:: +The interval to perform background checks of connections in the pool. +Defaults to `60s`. + `group_search.base_dn`:: The context to search for groups in which the user has membership. Defaults to the root of the Active Directory domain. diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java index 4a0d44a452f..506454a8a22 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolver.java @@ -24,9 +24,13 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; +import static org.elasticsearch.xpack.security.authc.ldap.ActiveDirectorySessionFactory.AD_DOMAIN_NAME_SETTING; +import static org.elasticsearch.xpack.security.authc.ldap.ActiveDirectorySessionFactory.buildDnFromDomain; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.OBJECT_CLASS_PRESENCE_FILTER; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.search; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry; +import static org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory.IGNORE_REFERRAL_ERRORS_SETTING; + class ActiveDirectoryGroupsResolver implements GroupsResolver { @@ -35,11 +39,10 @@ class ActiveDirectoryGroupsResolver implements GroupsResolver { private final LdapSearchScope scope; private final boolean ignoreReferralErrors; - ActiveDirectoryGroupsResolver(Settings settings, String baseDnDefault, - boolean ignoreReferralErrors) { - this.baseDn = settings.get("base_dn", baseDnDefault); - this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE); - this.ignoreReferralErrors = ignoreReferralErrors; + ActiveDirectoryGroupsResolver(Settings settings) { + this.baseDn = settings.get("group_search.base_dn", buildDnFromDomain(settings.get(AD_DOMAIN_NAME_SETTING))); + this.scope = LdapSearchScope.resolve(settings.get("group_search.scope"), LdapSearchScope.SUB_TREE); + this.ignoreReferralErrors = IGNORE_REFERRAL_ERRORS_SETTING.get(settings); } @Override diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java index 8483cc65f47..e55c00892d9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactory.java @@ -8,8 +8,12 @@ package org.elasticsearch.xpack.security.authc.ldap; import com.unboundid.ldap.sdk.Filter; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnectionOptions; +import com.unboundid.ldap.sdk.LDAPConnectionPool; import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.LDAPInterface; import com.unboundid.ldap.sdk.SearchResultEntry; +import com.unboundid.ldap.sdk.SimpleBindRequest; +import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl; import org.apache.logging.log4j.Logger; import org.apache.lucene.util.IOUtils; import org.elasticsearch.ElasticsearchSecurityException; @@ -27,6 +31,7 @@ import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession.GroupsResolver; import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils; import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; +import org.elasticsearch.xpack.security.authc.support.CharArrays; import org.elasticsearch.xpack.ssl.SSLService; import java.util.HashSet; @@ -46,7 +51,7 @@ import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.sear * user entry in Active Directory that matches the user name). This eliminates the need for user templates, and simplifies * the configuration for windows admins that may not be familiar with LDAP concepts. */ -class ActiveDirectorySessionFactory extends SessionFactory { +class ActiveDirectorySessionFactory extends PoolingSessionFactory { static final String AD_DOMAIN_NAME_SETTING = "domain_name"; @@ -58,29 +63,42 @@ class ActiveDirectorySessionFactory extends SessionFactory { static final String AD_DOWN_LEVEL_USER_SEARCH_FILTER_SETTING = "user_search.down_level_filter"; static final String AD_USER_SEARCH_SCOPE_SETTING = "user_search.scope"; private static final String NETBIOS_NAME_FILTER_TEMPLATE = "(netbiosname={0})"; + private static final Setting POOL_ENABLED = Setting.boolSetting("user_search.pool.enabled", + settings -> Boolean.toString(PoolingSessionFactory.BIND_DN.exists(settings)), Setting.Property.NodeScope); final DefaultADAuthenticator defaultADAuthenticator; final DownLevelADAuthenticator downLevelADAuthenticator; final UpnADAuthenticator upnADAuthenticator; - ActiveDirectorySessionFactory(RealmConfig config, SSLService sslService) { - super(config, sslService); + ActiveDirectorySessionFactory(RealmConfig config, SSLService sslService) throws LDAPException { + super(config, sslService, new ActiveDirectoryGroupsResolver(config.settings()), POOL_ENABLED, () -> { + if (BIND_DN.exists(config.settings())) { + return new SimpleBindRequest(getBindDN(config.settings()), BIND_PASSWORD.get(config.settings())); + } else { + return new SimpleBindRequest(); + } + }, () -> { + if (BIND_DN.exists(config.settings())) { + final String healthCheckDn = BIND_DN.get(config.settings()); + if (healthCheckDn.isEmpty() && healthCheckDn.indexOf('=') > 0) { + return healthCheckDn; + } + } + return config.settings().get(AD_USER_SEARCH_BASEDN_SETTING, config.settings().get(AD_DOMAIN_NAME_SETTING)); + }); Settings settings = config.settings(); String domainName = settings.get(AD_DOMAIN_NAME_SETTING); if (domainName == null) { - throw new IllegalArgumentException("missing [" + AD_DOMAIN_NAME_SETTING + - "] setting for active directory"); + throw new IllegalArgumentException("missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory"); } String domainDN = buildDnFromDomain(domainName); - GroupsResolver groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN, - ignoreReferralErrors); - LdapMetaDataResolver metaDataResolver = new LdapMetaDataResolver(config.settings(), ignoreReferralErrors); - defaultADAuthenticator = new DefaultADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver, + defaultADAuthenticator = new DefaultADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver, metaDataResolver, domainDN); downLevelADAuthenticator = new DownLevelADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver, metaDataResolver, domainDN, sslService); upnADAuthenticator = new UpnADAuthenticator(config, timeout, ignoreReferralErrors, logger, groupResolver, metaDataResolver, domainDN); + } @Override @@ -88,30 +106,78 @@ class ActiveDirectorySessionFactory extends SessionFactory { return new String[] {"ldap://" + settings.get(AD_DOMAIN_NAME_SETTING) + ":389"}; } - /** - * This is an active directory bind that looks up the user DN after binding with a windows principal. - * - * @param username name of the windows user without the domain - */ @Override - public void session(String username, SecureString password, ActionListener listener) { + void getSessionWithPool(LDAPConnectionPool connectionPool, String user, SecureString password, ActionListener listener) { + getADAuthenticator(user).authenticate(connectionPool, user, password, listener); + } + + @Override + void getSessionWithoutPool(String username, SecureString password, ActionListener listener) { // the runnable action here allows us make the control/flow logic simpler to understand. If we got a connection then lets // authenticate. If there was a failure pass it back using the listener Runnable runnable; try { final LDAPConnection connection = LdapUtils.privilegedConnect(serverSet::getConnection); runnable = () -> getADAuthenticator(username).authenticate(connection, username, password, - ActionListener.wrap(listener::onResponse, - (e) -> { - IOUtils.closeWhileHandlingException(connection); - listener.onFailure(e); - })); + ActionListener.wrap(listener::onResponse, + (e) -> { + IOUtils.closeWhileHandlingException(connection); + listener.onFailure(e); + })); } catch (LDAPException e) { runnable = () -> listener.onFailure(e); } runnable.run(); } + @Override + void getUnauthenticatedSessionWithPool(LDAPConnectionPool connectionPool, String user, ActionListener listener) { + getADAuthenticator(user).searchForDN(connectionPool, user, null, Math.toIntExact(timeout.seconds()), ActionListener.wrap(entry -> { + if (entry == null) { + listener.onResponse(null); + } else { + final String dn = entry.getDN(); + listener.onResponse(new LdapSession(logger, config, connectionPool, dn, groupResolver, metaDataResolver, timeout, null)); + } + }, listener::onFailure)); + } + + @Override + void getUnauthenticatedSessionWithoutPool(String user, ActionListener listener) { + if (BIND_DN.exists(config.settings())) { + LDAPConnection connection = null; + boolean startedSearching = false; + try { + connection = LdapUtils.privilegedConnect(serverSet::getConnection); + connection.bind(new SimpleBindRequest(getBindDN(config.settings()), BIND_PASSWORD.get(config.settings()))); + final LDAPConnection finalConnection = connection; + getADAuthenticator(user).searchForDN(finalConnection, user, null, Math.toIntExact(timeout.getSeconds()), + ActionListener.wrap(entry -> { + if (entry == null) { + IOUtils.closeWhileHandlingException(finalConnection); + listener.onResponse(null); + } else { + final String dn = entry.getDN(); + listener.onResponse(new LdapSession(logger, config, finalConnection, dn, groupResolver, metaDataResolver, + timeout, null)); + } + }, e -> { + IOUtils.closeWhileHandlingException(finalConnection); + listener.onFailure(e); + })); + startedSearching = true; + } catch (LDAPException e) { + listener.onFailure(e); + } finally { + if (connection != null && startedSearching == false) { + IOUtils.closeWhileHandlingException(connection); + } + } + } else { + listener.onResponse(null); + } + } + /** * @param domain active directory domain name * @return LDAP DN, distinguished name, of the root of the domain @@ -120,6 +186,14 @@ class ActiveDirectorySessionFactory extends SessionFactory { return "DC=" + domain.replace(".", ",DC="); } + static String getBindDN(Settings settings) { + String bindDN = BIND_DN.get(settings); + if (bindDN.isEmpty() == false && bindDN.indexOf('\\') < 0 && bindDN.indexOf('@') < 0) { + bindDN = bindDN + "@" + settings.get(AD_DOMAIN_NAME_SETTING); + } + return bindDN; + } + public static Set> getSettings() { Set> settings = new HashSet<>(); settings.addAll(SessionFactory.getSettings()); @@ -131,6 +205,7 @@ class ActiveDirectorySessionFactory extends SessionFactory { settings.add(Setting.simpleString(AD_UPN_USER_SEARCH_FILTER_SETTING, Setting.Property.NodeScope)); settings.add(Setting.simpleString(AD_DOWN_LEVEL_USER_SEARCH_FILTER_SETTING, Setting.Property.NodeScope)); settings.add(Setting.simpleString(AD_USER_SEARCH_SCOPE_SETTING, Setting.Property.NodeScope)); + settings.addAll(PoolingSessionFactory.getSettings()); return settings; } @@ -154,6 +229,8 @@ class ActiveDirectorySessionFactory extends SessionFactory { final String userSearchDN; final LdapSearchScope userSearchScope; final String userSearchFilter; + final String bindDN; + final String bindPassword; // TODO this needs to be a setting in the secure settings store! ADAuthenticator(RealmConfig realm, TimeValue timeout, boolean ignoreReferralErrors, Logger logger, GroupsResolver groupsResolver, LdapMetaDataResolver metaDataResolver, String domainDN, @@ -165,6 +242,8 @@ class ActiveDirectorySessionFactory extends SessionFactory { this.groupsResolver = groupsResolver; this.metaDataResolver = metaDataResolver; final Settings settings = realm.settings(); + this.bindDN = getBindDN(settings); + this.bindPassword = BIND_PASSWORD.get(settings); userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN); userSearchScope = LdapSearchScope.resolve(settings.get(AD_USER_SEARCH_SCOPE_SETTING), LdapSearchScope.SUB_TREE); userSearchFilter = settings.get(userSearchFilterSetting, defaultUserSearchFilter); @@ -174,7 +253,11 @@ class ActiveDirectorySessionFactory extends SessionFactory { ActionListener listener) { boolean success = false; try { - connection.bind(bindUsername(username), new String(password.getChars())); + connection.bind(new SimpleBindRequest(bindUsername(username), CharArrays.toUtf8Bytes(password.getChars()), + new AuthorizationIdentityRequestControl())); + if (bindDN.isEmpty() == false) { + connection.bind(new SimpleBindRequest(bindDN, bindPassword)); + } searchForDN(connection, username, password, Math.toIntExact(timeout.seconds()), ActionListener.wrap((entry) -> { if (entry == null) { IOUtils.close(connection); @@ -200,6 +283,28 @@ class ActiveDirectorySessionFactory extends SessionFactory { } } + final void authenticate(LDAPConnectionPool pool, String username, SecureString password, + ActionListener listener) { + try { + LdapUtils.privilegedConnect(() -> { + SimpleBindRequest request = new SimpleBindRequest(bindUsername(username), CharArrays.toUtf8Bytes(password.getChars())); + return pool.bindAndRevertAuthentication(request); + }); + searchForDN(pool, username, password, Math.toIntExact(timeout.seconds()), ActionListener.wrap((entry) -> { + if (entry == null) { + // we did not find the user, cannot authenticate in this realm + listener.onFailure(new ElasticsearchSecurityException("search for user [" + username + + "] by principle name yielded no results")); + } else { + final String dn = entry.getDN(); + listener.onResponse(new LdapSession(logger, realm, pool, dn, groupsResolver, metaDataResolver, timeout, null)); + } + }, listener::onFailure)); + } catch (LDAPException e) { + listener.onFailure(e); + } + } + String bindUsername(String username) { return username; } @@ -209,7 +314,7 @@ class ActiveDirectorySessionFactory extends SessionFactory { return userSearchFilter; } - abstract void searchForDN(LDAPConnection connection, String username, SecureString password, int timeLimitSeconds, + abstract void searchForDN(LDAPInterface connection, String username, SecureString password, int timeLimitSeconds, ActionListener listener); } @@ -233,7 +338,7 @@ class ActiveDirectorySessionFactory extends SessionFactory { } @Override - void searchForDN(LDAPConnection connection, String username, SecureString password, + void searchForDN(LDAPInterface connection, String username, SecureString password, int timeLimitSeconds, ActionListener listener) { try { searchForEntry(connection, userSearchDN, userSearchScope.scope(), @@ -276,7 +381,7 @@ class ActiveDirectorySessionFactory extends SessionFactory { } @Override - void searchForDN(LDAPConnection connection, String username, SecureString password, int timeLimitSeconds, + void searchForDN(LDAPInterface connection, String username, SecureString password, int timeLimitSeconds, ActionListener listener) { String[] parts = username.split("\\\\"); assert parts.length == 2; @@ -285,7 +390,6 @@ class ActiveDirectorySessionFactory extends SessionFactory { netBiosDomainNameToDn(connection, netBiosDomainName, username, password, timeLimitSeconds, ActionListener.wrap((domainDN) -> { if (domainDN == null) { - IOUtils.close(connection); listener.onResponse(null); } else { try { @@ -294,75 +398,75 @@ class ActiveDirectorySessionFactory extends SessionFactory { accountName), timeLimitSeconds, ignoreReferralErrors, listener, attributesToSearchFor(groupsResolver.attributes())); } catch (LDAPException e) { - IOUtils.closeWhileHandlingException(connection); listener.onFailure(e); } } - }, (e) -> { - IOUtils.closeWhileHandlingException(connection); - listener.onFailure(e); - })); + }, listener::onFailure)); } - void netBiosDomainNameToDn(LDAPConnection connection, String netBiosDomainName, String username, SecureString password, + void netBiosDomainNameToDn(LDAPInterface ldapInterface, String netBiosDomainName, String username, SecureString password, int timeLimitSeconds, ActionListener listener) { final String cachedName = domainNameCache.get(netBiosDomainName); - if (cachedName != null) { - listener.onResponse(cachedName); - } else if (usingGlobalCatalog(settings, connection)) { - // the global catalog does not replicate the necessary information to map a netbios - // dns name to a DN so we need to instead connect to the normal ports. This code - // uses the standard ports to avoid adding even more settings and is probably ok as - // most AD users do not use non-standard ports - final LDAPConnectionOptions options = connectionOptions(config, sslService, logger); - boolean startedSearching = false; - LDAPConnection searchConnection = null; - try { - Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); - if (connection.getSSLSession() != null) { + try { + if (cachedName != null) { + listener.onResponse(cachedName); + } else if (usingGlobalCatalog(ldapInterface)) { + // the global catalog does not replicate the necessary information to map a netbios + // dns name to a DN so we need to instead connect to the normal ports. This code + // uses the standard ports to avoid adding even more settings and is probably ok as + // most AD users do not use non-standard ports + final LDAPConnectionOptions options = connectionOptions(config, sslService, logger); + boolean startedSearching = false; + LDAPConnection searchConnection = null; + LDAPConnection ldapConnection = null; + try { + Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); + if (ldapInterface instanceof LDAPConnection) { + ldapConnection = (LDAPConnection) ldapInterface; + } else { + ldapConnection = LdapUtils.privilegedConnect(((LDAPConnectionPool) ldapInterface)::getConnection); + } + final LDAPConnection finalLdapConnection = ldapConnection; searchConnection = LdapUtils.privilegedConnect( - () -> new LDAPConnection(connection.getSocketFactory(), options, - connection.getConnectedAddress(), 636)); - } else { - searchConnection = LdapUtils.privilegedConnect(() -> - new LDAPConnection(options, connection.getConnectedAddress(), 389)); + () -> new LDAPConnection(finalLdapConnection.getSocketFactory(), options, + finalLdapConnection.getConnectedAddress(), + finalLdapConnection.getSSLSession() != null ? 636 : 389)); + + final SimpleBindRequest bindRequest = + bindDN.isEmpty() ? new SimpleBindRequest(username, CharArrays.toUtf8Bytes(password.getChars())) : + new SimpleBindRequest(bindDN, bindPassword); + searchConnection.bind(bindRequest); + final LDAPConnection finalConnection = searchConnection; + search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, + timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap( + (results) -> { + IOUtils.close(finalConnection); + handleSearchResults(results, netBiosDomainName, domainNameCache, listener); + }, (e) -> { + IOUtils.closeWhileHandlingException(finalConnection); + listener.onFailure(e); + }), + "ncname"); + startedSearching = true; + } finally { + if (startedSearching == false) { + IOUtils.closeWhileHandlingException(searchConnection); + } + if (ldapInterface instanceof LDAPConnectionPool && ldapConnection != null) { + ((LDAPConnectionPool) ldapInterface).releaseConnection(ldapConnection); + } } - searchConnection.bind(username, new String(password.getChars())); - final LDAPConnection finalConnection = searchConnection; - search(finalConnection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, - timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap( - (results) -> { - IOUtils.close(finalConnection); - handleSearchResults(results, netBiosDomainName, - domainNameCache, listener); - }, (e) -> { - IOUtils.closeWhileHandlingException(connection); - listener.onFailure(e); - }), - "ncname"); - startedSearching = true; - } catch (LDAPException e) { - listener.onFailure(e); - } finally { - if (startedSearching == false) { - IOUtils.closeWhileHandlingException(searchConnection); - } - } - } else { - try { + } else { Filter filter = createFilter(NETBIOS_NAME_FILTER_TEMPLATE, netBiosDomainName); - search(connection, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, + search(ldapInterface, domainDN, LdapSearchScope.SUB_TREE.scope(), filter, timeLimitSeconds, ignoreReferralErrors, ActionListener.wrap( (results) -> handleSearchResults(results, netBiosDomainName, domainNameCache, listener), - (e) -> { - IOUtils.closeWhileHandlingException(connection); - listener.onFailure(e); - }), + listener::onFailure), "ncname"); - } catch (LDAPException e) { - listener.onFailure(e); } + } catch (LDAPException e) { + listener.onFailure(e); } } @@ -385,15 +489,32 @@ class ActiveDirectorySessionFactory extends SessionFactory { } } - static boolean usingGlobalCatalog(Settings settings, LDAPConnection ldapConnection) { - Boolean usingGlobalCatalog = settings.getAsBoolean("global_catalog", null); - if (usingGlobalCatalog != null) { - return usingGlobalCatalog; + static boolean usingGlobalCatalog(LDAPInterface ldap) throws LDAPException { + if (ldap instanceof LDAPConnection) { + return usingGlobalCatalog((LDAPConnection) ldap); + } else { + LDAPConnectionPool pool = (LDAPConnectionPool) ldap; + LDAPConnection connection = null; + try { + connection = LdapUtils.privilegedConnect(pool::getConnection); + return usingGlobalCatalog(connection); + } finally { + if (connection != null) { + pool.releaseConnection(connection); + } + } } + } + + private static boolean usingGlobalCatalog(LDAPConnection ldapConnection) { return ldapConnection.getConnectedPort() == 3268 || ldapConnection.getConnectedPort() == 3269; } } + /** + * Authenticates user principal names provided by the user (eq user@domain). Note this authenticator does not currently support + * UPN suffixes that are different than the actual domain name. + */ static class UpnADAuthenticator extends ADAuthenticator { static final String UPN_USER_FILTER = "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={1})))"; @@ -404,7 +525,7 @@ class ActiveDirectorySessionFactory extends SessionFactory { AD_UPN_USER_SEARCH_FILTER_SETTING, UPN_USER_FILTER); } - void searchForDN(LDAPConnection connection, String username, SecureString password, int timeLimitSeconds, + void searchForDN(LDAPInterface connection, String username, SecureString password, int timeLimitSeconds, ActionListener listener) { String[] parts = username.split("@"); assert parts.length == 2; diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java index a217521eb26..553e13e67e9 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactory.java @@ -6,25 +6,19 @@ package org.elasticsearch.xpack.security.authc.ldap; import com.unboundid.ldap.sdk.Filter; -import com.unboundid.ldap.sdk.GetEntryLDAPConnectionPoolHealthCheck; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnectionPool; -import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPInterface; import com.unboundid.ldap.sdk.SearchResultEntry; -import com.unboundid.ldap.sdk.ServerSet; import com.unboundid.ldap.sdk.SimpleBindRequest; -import org.apache.logging.log4j.Logger; import org.apache.lucene.util.IOUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.xpack.security.authc.RealmConfig; import org.elasticsearch.xpack.security.authc.RealmSettings; -import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; 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.GroupsResolver; @@ -36,7 +30,6 @@ import org.elasticsearch.xpack.ssl.SSLService; import java.util.Arrays; import java.util.HashSet; -import java.util.Optional; import java.util.Set; import java.util.function.Function; @@ -44,12 +37,9 @@ import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.attr import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.createFilter; import static org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils.searchForEntry; -class LdapUserSearchSessionFactory extends SessionFactory { +class LdapUserSearchSessionFactory extends PoolingSessionFactory { - static final int DEFAULT_CONNECTION_POOL_SIZE = 20; - static final int DEFAULT_CONNECTION_POOL_INITIAL_SIZE = 0; - static final String DEFAULT_USERNAME_ATTRIBUTE = "uid"; - static final TimeValue DEFAULT_HEALTH_CHECK_INTERVAL = TimeValue.timeValueSeconds(60L); + private static final String DEFAULT_USERNAME_ATTRIBUTE = "uid"; static final String SEARCH_PREFIX = "user_search."; static final Setting SEARCH_ATTRIBUTE = new Setting<>("user_search.attribute", DEFAULT_USERNAME_ATTRIBUTE, @@ -59,36 +49,22 @@ class LdapUserSearchSessionFactory extends SessionFactory { private static final Setting SEARCH_FILTER = Setting.simpleString("user_search.filter", Setting.Property.NodeScope); private static final Setting SEARCH_SCOPE = new Setting<>("user_search.scope", (String) null, s -> LdapSearchScope.resolve(s, LdapSearchScope.SUB_TREE), Setting.Property.NodeScope); - - private static final Setting POOL_ENABLED = Setting.boolSetting("user_search.pool.enabled", - true, Setting.Property.NodeScope); - private static final Setting POOL_INITIAL_SIZE = Setting.intSetting("user_search.pool.initial_size", - DEFAULT_CONNECTION_POOL_INITIAL_SIZE, 0, Setting.Property.NodeScope); - private static final Setting POOL_SIZE = Setting.intSetting("user_search.pool.size", - DEFAULT_CONNECTION_POOL_SIZE, 1, Setting.Property.NodeScope); - private static final Setting HEALTH_CHECK_INTERVAL = Setting.timeSetting("user_search.pool.health_check.interval", - DEFAULT_HEALTH_CHECK_INTERVAL, Setting.Property.NodeScope); - private static final Setting HEALTH_CHECK_ENABLED = Setting.boolSetting("user_search.pool.health_check.enabled", - true, Setting.Property.NodeScope); - private static final Setting> HEALTH_CHECK_DN = new Setting<>("user_search.pool.health_check.dn", (String) null, - Optional::ofNullable, Setting.Property.NodeScope); - - private static final Setting BIND_DN = Setting.simpleString("bind_dn", - Setting.Property.NodeScope, Setting.Property.Filtered); - private static final Setting BIND_PASSWORD = Setting.simpleString("bind_password", - Setting.Property.NodeScope, Setting.Property.Filtered); + private static final Setting POOL_ENABLED = Setting.boolSetting("user_search.pool.enabled", true, Setting.Property.NodeScope); private final String userSearchBaseDn; private final LdapSearchScope scope; private final String searchFilter; - private final GroupsResolver groupResolver; - private final boolean useConnectionPool; - - private final LDAPConnectionPool connectionPool; - private final LdapMetaDataResolver metaDataResolver; LdapUserSearchSessionFactory(RealmConfig config, SSLService sslService) throws LDAPException { - super(config, sslService); + super(config, sslService, groupResolver(config.settings()), POOL_ENABLED, + () -> LdapUserSearchSessionFactory.bindRequest(config.settings()), + () -> { + if (BIND_DN.exists(config.settings())) { + return BIND_DN.get(config.settings()); + } else { + return SEARCH_BASE_DN.get(config.settings()); + } + }); Settings settings = config.settings(); if (SEARCH_BASE_DN.exists(settings)) { userSearchBaseDn = SEARCH_BASE_DN.get(settings); @@ -97,57 +73,10 @@ class LdapUserSearchSessionFactory extends SessionFactory { } scope = SEARCH_SCOPE.get(settings); searchFilter = getSearchFilter(config); - groupResolver = groupResolver(settings); - metaDataResolver = new LdapMetaDataResolver(config.settings(), ignoreReferralErrors); - useConnectionPool = POOL_ENABLED.get(settings); - if (useConnectionPool) { - connectionPool = createConnectionPool(config, serverSet, timeout, logger); - } else { - connectionPool = null; - } logger.info("Realm [{}] is in user-search mode - base_dn=[{}], search filter=[{}]", config.name(), userSearchBaseDn, searchFilter); } - static LDAPConnectionPool createConnectionPool(RealmConfig config, ServerSet serverSet, TimeValue timeout, Logger logger) - throws LDAPException { - Settings settings = config.settings(); - SimpleBindRequest bindRequest = bindRequest(settings); - final int initialSize = POOL_INITIAL_SIZE.get(settings); - final int size = POOL_SIZE.get(settings); - LDAPConnectionPool pool = null; - boolean success = false; - try { - pool = LdapUtils.privilegedConnect(() -> new LDAPConnectionPool(serverSet, bindRequest, initialSize, size)); - pool.setRetryFailedOperationsDueToInvalidConnections(true); - if (HEALTH_CHECK_ENABLED.get(settings)) { - String entryDn = HEALTH_CHECK_DN.get(settings).orElseGet(() -> bindRequest == null ? null : bindRequest.getBindDN()); - final long healthCheckInterval = HEALTH_CHECK_INTERVAL.get(settings).millis(); - if (entryDn != null) { - // 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 and a bind request has not been executed - // yet so we could end up never getting a connection. 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 - LDAPConnectionPoolHealthCheck healthCheck = new GetEntryLDAPConnectionPoolHealthCheck(entryDn, timeout.millis(), - false, false, false, true, false); - pool.setHealthCheck(healthCheck); - pool.setHealthCheckIntervalMillis(healthCheckInterval); - } else { - logger.warn("[" + RealmSettings.getFullSettingKey(config, BIND_DN) + "] and [" + - RealmSettings.getFullSettingKey(config, HEALTH_CHECK_DN) + "] have not been specified so no " + - "ldap query will be run as a health check"); - } - } - - success = true; - return pool; - } finally { - if (success == false && pool != null) { - pool.close(); - } - } - } - static SimpleBindRequest bindRequest(Settings settings) { if (BIND_DN.exists(settings)) { return new SimpleBindRequest(BIND_DN.get(settings), BIND_PASSWORD.get(settings)); @@ -156,23 +85,15 @@ class LdapUserSearchSessionFactory extends SessionFactory { } } - public static boolean hasUserSearchSettings(RealmConfig config) { + static boolean hasUserSearchSettings(RealmConfig config) { return config.settings().getByPrefix("user_search.").isEmpty() == false; } - @Override - public void session(String user, SecureString password, ActionListener listener) { - if (useConnectionPool) { - getSessionWithPool(user, password, listener); - } else { - getSessionWithoutPool(user, password, listener); - } - } - /** * Sets up a LDAPSession using the connection pool that potentially holds existing connections to the server */ - private void getSessionWithPool(String user, SecureString password, ActionListener listener) { + @Override + void getSessionWithPool(LDAPConnectionPool connectionPool, String user, SecureString password, ActionListener listener) { findUser(user, connectionPool, ActionListener.wrap((entry) -> { if (entry == null) { listener.onResponse(null); @@ -204,7 +125,8 @@ class LdapUserSearchSessionFactory extends SessionFactory { *
  • Creates a new LDAPSession with the bound connection
  • * */ - private void getSessionWithoutPool(String user, SecureString password, ActionListener listener) { + @Override + void getSessionWithoutPool(String user, SecureString password, ActionListener listener) { boolean success = false; LDAPConnection connection = null; try { @@ -261,33 +183,42 @@ class LdapUserSearchSessionFactory extends SessionFactory { } @Override - public void unauthenticatedSession(String user, ActionListener listener) { + void getUnauthenticatedSessionWithPool(LDAPConnectionPool connectionPool, String user, ActionListener listener) { + findUser(user, connectionPool, ActionListener.wrap((entry) -> { + if (entry == null) { + listener.onResponse(null); + } else { + final String dn = entry.getDN(); + LdapSession session = new LdapSession(logger, config, connectionPool, dn, groupResolver, metaDataResolver, timeout, + entry.getAttributes()); + listener.onResponse(session); + } + }, listener::onFailure)); + } + + @Override + void getUnauthenticatedSessionWithoutPool(String user, ActionListener listener) { LDAPConnection connection = null; boolean success = false; try { - final LDAPInterface ldapInterface; - if (useConnectionPool) { - ldapInterface = connectionPool; - } else { - connection = LdapUtils.privilegedConnect(serverSet::getConnection); - connection.bind(bindRequest(config.settings())); - ldapInterface = connection; - } + connection = LdapUtils.privilegedConnect(serverSet::getConnection); + connection.bind(bindRequest(config.settings())); + final LDAPConnection finalConnection = connection; - findUser(user, ldapInterface, ActionListener.wrap((entry) -> { + findUser(user, finalConnection, ActionListener.wrap((entry) -> { if (entry == null) { listener.onResponse(null); } else { boolean sessionCreated = false; try { final String dn = entry.getDN(); - LdapSession session = new LdapSession(logger, config, ldapInterface, dn, groupResolver, metaDataResolver, timeout, + LdapSession session = new LdapSession(logger, config, finalConnection, dn, groupResolver, metaDataResolver, timeout, entry.getAttributes()); sessionCreated = true; listener.onResponse(session); } finally { - if (sessionCreated == false && useConnectionPool == false) { - IOUtils.close((LDAPConnection) ldapInterface); + if (sessionCreated == false) { + IOUtils.close(finalConnection); } } } @@ -316,16 +247,7 @@ class LdapUserSearchSessionFactory extends SessionFactory { attributesToSearchFor(groupResolver.attributes(), metaDataResolver.attributeNames())); } - /* - * This method is used to cleanup the connections - */ - void shutdown() { - if (connectionPool != null) { - connectionPool.close(); - } - } - - static GroupsResolver groupResolver(Settings settings) { + private static GroupsResolver groupResolver(Settings settings) { if (SearchGroupsResolver.BASE_DN.exists(settings)) { return new SearchGroupsResolver(settings); } @@ -352,17 +274,11 @@ class LdapUserSearchSessionFactory extends SessionFactory { public static Set> getSettings() { Set> settings = new HashSet<>(); settings.addAll(SessionFactory.getSettings()); + settings.addAll(PoolingSessionFactory.getSettings()); settings.add(SEARCH_BASE_DN); settings.add(SEARCH_SCOPE); settings.add(SEARCH_ATTRIBUTE); settings.add(POOL_ENABLED); - settings.add(POOL_INITIAL_SIZE); - settings.add(POOL_SIZE); - settings.add(HEALTH_CHECK_ENABLED); - settings.add(HEALTH_CHECK_DN); - settings.add(HEALTH_CHECK_INTERVAL); - settings.add(BIND_DN); - settings.add(BIND_PASSWORD); settings.add(SEARCH_FILTER); settings.addAll(SearchGroupsResolver.getSettings()); diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java new file mode 100644 index 00000000000..253bdb64ca1 --- /dev/null +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/PoolingSessionFactory.java @@ -0,0 +1,185 @@ +/* + * 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 com.unboundid.ldap.sdk.BindRequest; +import com.unboundid.ldap.sdk.GetEntryLDAPConnectionPoolHealthCheck; +import com.unboundid.ldap.sdk.LDAPConnectionPool; +import com.unboundid.ldap.sdk.LDAPConnectionPoolHealthCheck; +import com.unboundid.ldap.sdk.LDAPException; +import com.unboundid.ldap.sdk.ServerSet; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.lease.Releasable; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.xpack.security.authc.RealmConfig; +import org.elasticsearch.xpack.security.authc.RealmSettings; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapMetaDataResolver; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapSession; +import org.elasticsearch.xpack.security.authc.ldap.support.LdapUtils; +import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; +import org.elasticsearch.xpack.ssl.SSLService; + +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Base class for LDAP session factories that can make use of a connection pool + */ +abstract class PoolingSessionFactory extends SessionFactory implements Releasable { + + static final int DEFAULT_CONNECTION_POOL_SIZE = 20; + static final int DEFAULT_CONNECTION_POOL_INITIAL_SIZE = 0; + static final Setting BIND_DN = Setting.simpleString("bind_dn", Setting.Property.NodeScope, Setting.Property.Filtered); + static final Setting BIND_PASSWORD = Setting.simpleString("bind_password", Setting.Property.NodeScope, + Setting.Property.Filtered); + + private static final TimeValue DEFAULT_HEALTH_CHECK_INTERVAL = TimeValue.timeValueSeconds(60L); + private static final Setting POOL_INITIAL_SIZE = Setting.intSetting("user_search.pool.initial_size", + DEFAULT_CONNECTION_POOL_INITIAL_SIZE, 0, Setting.Property.NodeScope); + private static final Setting POOL_SIZE = Setting.intSetting("user_search.pool.size", + DEFAULT_CONNECTION_POOL_SIZE, 1, Setting.Property.NodeScope); + private static final Setting HEALTH_CHECK_INTERVAL = Setting.timeSetting("user_search.pool.health_check.interval", + DEFAULT_HEALTH_CHECK_INTERVAL, Setting.Property.NodeScope); + private static final Setting HEALTH_CHECK_ENABLED = Setting.boolSetting("user_search.pool.health_check.enabled", + true, Setting.Property.NodeScope); + private static final Setting> HEALTH_CHECK_DN = new Setting<>("user_search.pool.health_check.dn", (String) null, + Optional::ofNullable, Setting.Property.NodeScope); + + private final boolean useConnectionPool; + private final LDAPConnectionPool connectionPool; + + final LdapMetaDataResolver metaDataResolver; + final LdapSession.GroupsResolver groupResolver; + + + /** + * @param config the configuration for the realm + * @param sslService the ssl service to get a socket factory or context from + * @param groupResolver the resolver to use to find groups belonging to a user + * @param poolingEnabled the setting that should be used to determine if connection pooling is enabled + * @param bindRequestSupplier the supplier for a bind requests that should be used for pooled connections + * @param healthCheckDNSupplier a supplier for the dn to query for health checks + */ + PoolingSessionFactory(RealmConfig config, SSLService sslService, LdapSession.GroupsResolver groupResolver, + Setting poolingEnabled, Supplier bindRequestSupplier, + Supplier healthCheckDNSupplier) throws LDAPException { + super(config, sslService); + this.groupResolver = groupResolver; + this.metaDataResolver = new LdapMetaDataResolver(config.settings(), ignoreReferralErrors); + this.useConnectionPool = poolingEnabled.get(config.settings()); + if (useConnectionPool) { + this.connectionPool = createConnectionPool(config, serverSet, timeout, logger, bindRequestSupplier, healthCheckDNSupplier); + } else { + this.connectionPool = null; + } + } + + @Override + public final void session(String user, SecureString password, ActionListener listener) { + if (useConnectionPool) { + getSessionWithPool(connectionPool, user, password, listener); + } else { + getSessionWithoutPool(user, password, listener); + } + } + + @Override + public final void unauthenticatedSession(String user, ActionListener listener) { + if (useConnectionPool) { + getUnauthenticatedSessionWithPool(connectionPool, user, listener); + } else { + getUnauthenticatedSessionWithoutPool(user, listener); + } + } + + /** + * Attempts to get a {@link LdapSession} using the provided credentials and makes use of the provided connection pool + */ + abstract void getSessionWithPool(LDAPConnectionPool connectionPool, String user, SecureString password, + ActionListener listener); + + /** + * Attempts to get a {@link LdapSession} using the provided credentials and opens a new connection to the ldap server + */ + abstract void getSessionWithoutPool(String user, SecureString password, ActionListener listener); + + /** + * Attempts to search using a pooled connection for the user and provides an unauthenticated {@link LdapSession} to the listener if the + * user is found + */ + abstract void getUnauthenticatedSessionWithPool(LDAPConnectionPool connectionPool, String user, ActionListener listener); + + /** + * Attempts to search using a new connection for the user and provides an unauthenticated {@link LdapSession} to the listener if the + * user is found + */ + abstract void getUnauthenticatedSessionWithoutPool(String user, ActionListener listener); + + /** + * Creates the connection pool that will be used by the session factory and initializes the health check support + */ + static LDAPConnectionPool createConnectionPool(RealmConfig config, ServerSet serverSet, TimeValue timeout, Logger logger, + Supplier bindRequestSupplier, + Supplier healthCheckDnSupplier) throws LDAPException { + Settings settings = config.settings(); + BindRequest bindRequest = bindRequestSupplier.get(); + final int initialSize = POOL_INITIAL_SIZE.get(settings); + final int size = POOL_SIZE.get(settings); + LDAPConnectionPool pool = null; + boolean success = false; + try { + pool = LdapUtils.privilegedConnect(() -> new LDAPConnectionPool(serverSet, bindRequest, initialSize, size)); + pool.setRetryFailedOperationsDueToInvalidConnections(true); + if (HEALTH_CHECK_ENABLED.get(settings)) { + String entryDn = HEALTH_CHECK_DN.get(settings).orElseGet(healthCheckDnSupplier); + final long healthCheckInterval = HEALTH_CHECK_INTERVAL.get(settings).millis(); + if (entryDn != null) { + // Checks the status of the LDAP connection at a specified interval in the background. We do not check on + // create as the LDAP server may require authentication to get an entry and a bind request has not been executed + // yet so we could end up never getting a connection. 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 + LDAPConnectionPoolHealthCheck healthCheck = new GetEntryLDAPConnectionPoolHealthCheck(entryDn, timeout.millis(), + false, false, false, true, false); + pool.setHealthCheck(healthCheck); + pool.setHealthCheckIntervalMillis(healthCheckInterval); + } else { + logger.warn(new ParameterizedMessage("[{}] and [{}} have not been specified or are not valid distinguished names," + + "so connection health checking is disabled", RealmSettings.getFullSettingKey(config, BIND_DN), + RealmSettings.getFullSettingKey(config, HEALTH_CHECK_DN))); + } + } + + success = true; + return pool; + } finally { + if (success == false && pool != null) { + pool.close(); + } + } + } + + /** + * This method is used to cleanup the connection pool if one is being used + */ + @Override + public final void close() { + if (connectionPool != null) { + connectionPool.close(); + } + } + + public static Set> getSettings() { + return Sets.newHashSet(POOL_INITIAL_SIZE, POOL_SIZE, HEALTH_CHECK_ENABLED, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_DN, BIND_DN, + BIND_PASSWORD); + } +} diff --git a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java index 5a14eecdf8c..666a4b06c24 100644 --- a/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java +++ b/plugin/src/main/java/org/elasticsearch/xpack/security/authc/ldap/support/LdapUtils.java @@ -48,8 +48,7 @@ import java.util.stream.Collectors; public final class LdapUtils { - public static final Filter OBJECT_CLASS_PRESENCE_FILTER = - Filter.createPresenceFilter("objectClass"); + public static final Filter OBJECT_CLASS_PRESENCE_FILTER = Filter.createPresenceFilter("objectClass"); private static final Logger LOGGER = ESLoggerFactory.getLogger(LdapUtils.class); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java index 256d474fdc9..deba662dec0 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectoryGroupsResolverTests.java @@ -29,10 +29,11 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase { public void testResolveSubTree() throws Exception { Settings settings = Settings.builder() - .put("scope", LdapSearchScope.SUB_TREE) + .put("group_search.scope", LdapSearchScope.SUB_TREE) + .put("group_search.base_dn", "DC=ad,DC=test,DC=elasticsearch,DC=com") + .put("domain_name", "ad.test.elasticsearch.com") .build(); - ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, - "DC=ad,DC=test,DC=elasticsearch,DC=com", false); + ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings); List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null); assertThat(groups, containsInAnyOrder( @@ -48,10 +49,10 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase { public void testResolveOneLevel() throws Exception { Settings settings = Settings.builder() .put("scope", LdapSearchScope.ONE_LEVEL) - .put("base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com") + .put("group_search.base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com") + .put("domain_name", "ad.test.elasticsearch.com") .build(); - ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, - "DC=ad,DC=test,DC=elasticsearch,DC=com", false); + ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings); List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null); assertThat(groups, hasItem(containsString("Users"))); @@ -59,11 +60,11 @@ public class ActiveDirectoryGroupsResolverTests extends GroupsResolverTestCase { public void testResolveBaseLevel() throws Exception { Settings settings = Settings.builder() - .put("scope", LdapSearchScope.BASE) - .put("base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com") + .put("group_search.scope", LdapSearchScope.BASE) + .put("group_search.base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com") + .put("domain_name", "ad.test.elasticsearch.com") .build(); - ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, - "DC=ad,DC=test,DC=elasticsearch,DC=com", false); + ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings); List groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null); assertThat(groups, hasItem(containsString("CN=Users,CN=Builtin"))); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java index 694beccd985..0fc2d7ad417 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/ActiveDirectorySessionFactoryTests.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.security.authc.ldap.support.SessionFactory; import org.elasticsearch.test.junit.annotations.Network; import org.elasticsearch.xpack.ssl.VerificationMode; +import java.util.Arrays; import java.util.List; import java.util.concurrent.ExecutionException; @@ -46,43 +47,45 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false), globalSettings, new ThreadContext(Settings.EMPTY)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, - sslService); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "ironman"; - try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { - List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("Geniuses"), - containsString("Billionaire"), - containsString("Playboy"), - containsString("Philanthropists"), - containsString("Avengers"), - containsString("SHIELD"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"), - containsString("Supers"))); + String userName = "ironman"; + try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { + List groups = groups(ldap); + assertThat(groups, containsInAnyOrder( + containsString("Geniuses"), + containsString("Billionaire"), + containsString("Playboy"), + containsString("Philanthropists"), + containsString("Avengers"), + containsString("SHIELD"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"), + containsString("Supers"))); + } } } public void testNetbiosAuth() throws Exception { final String adUrl = randomFrom("ldap://54.213.145.20:3268", "ldaps://54.213.145.20:3269", AD_LDAP_URL); - RealmConfig config = new RealmConfig("ad-test", buildAdSettings(adUrl, AD_DOMAIN, false), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = new RealmConfig("ad-test", buildAdSettings(adUrl, AD_DOMAIN, false), globalSettings, + new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "ades\\ironman"; - try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { - List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("Geniuses"), - containsString("Billionaire"), - containsString("Playboy"), - containsString("Philanthropists"), - containsString("Avengers"), - containsString("SHIELD"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"), - containsString("Supers"))); + String userName = "ades\\ironman"; + try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { + List groups = groups(ldap); + assertThat(groups, containsInAnyOrder( + containsString("Geniuses"), + containsString("Billionaire"), + containsString("Playboy"), + containsString("Philanthropists"), + containsString("Avengers"), + containsString("SHIELD"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"), + containsString("Supers"))); + } } } @@ -94,23 +97,27 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put("ssl.verification_mode", VerificationMode.CERTIFICATE) .put(SessionFactory.TIMEOUT_TCP_READ_SETTING, "1ms") .build(); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - PlainActionFuture> groups = new PlainActionFuture<>(); - session(sessionFactory, "ironman", SECURED_PASSWORD).groups(groups); - LDAPException expected = expectThrows(LDAPException.class, groups::actionGet); - assertThat(expected.getMessage(), containsString("A client-side timeout was encountered while waiting")); + PlainActionFuture> groups = new PlainActionFuture<>(); + session(sessionFactory, "ironman", SECURED_PASSWORD).groups(groups); + LDAPException expected = expectThrows(LDAPException.class, groups::actionGet); + assertThat(expected.getMessage(), containsString("A client-side timeout was encountered while waiting")); + } } public void testAdAuthAvengers() throws Exception { - RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false), globalSettings, + new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", }; - for(String user: users) { - try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) { - assertThat("group avenger test for user "+user, groups(ldap), hasItem(containsString("Avengers"))); + String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow"}; + for (String user : users) { + try (LdapSession ldap = session(sessionFactory, user, SECURED_PASSWORD)) { + assertThat("group avenger test for user " + user, groups(ldap), hasItem(containsString("Avengers"))); + } } } } @@ -119,21 +126,23 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI public void testAuthenticate() throws Exception { Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.ONE_LEVEL, false); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "hulk"; - try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { - List groups = groups(ldap); + String userName = "hulk"; + try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { + List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("Avengers"), - containsString("SHIELD"), - containsString("Geniuses"), - containsString("Philanthropists"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"), - containsString("Supers"))); + assertThat(groups, containsInAnyOrder( + containsString("Avengers"), + containsString("SHIELD"), + containsString("Geniuses"), + containsString("Philanthropists"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"), + containsString("Supers"))); + } } } @@ -141,21 +150,23 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI public void testAuthenticateBaseUserSearch() throws Exception { Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Bruce Banner, CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.BASE, false); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), + new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "hulk"; - try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { - List groups = groups(ldap); + String userName = "hulk"; + try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { + List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("Avengers"), - containsString("SHIELD"), - containsString("Geniuses"), - containsString("Philanthropists"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"), - containsString("Supers"))); + assertThat(groups, containsInAnyOrder( + containsString("Avengers"), + containsString("SHIELD"), + containsString("Geniuses"), + containsString("Philanthropists"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"), + containsString("Supers"))); + } } } @@ -167,14 +178,16 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI "CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com") .put(ActiveDirectorySessionFactory.AD_GROUP_SEARCH_SCOPE_SETTING, LdapSearchScope.BASE) .build(); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "hulk"; - try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { - List groups = groups(ldap); + String userName = "hulk"; + try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) { + List groups = groups(ldap); - assertThat(groups, hasItem(containsString("Avengers"))); + assertThat(groups, hasItem(containsString("Avengers"))); + } } } @@ -182,37 +195,41 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI public void testAuthenticateWithUserPrincipalName() throws Exception { Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.ONE_LEVEL, false); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - //Login with the UserPrincipalName - String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; - try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) { - List groups = groups(ldap); - assertThat(ldap.userDn(), is(userDN)); - assertThat(groups, containsInAnyOrder( - containsString("Geniuses"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"))); + //Login with the UserPrincipalName + String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; + try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) { + List groups = groups(ldap); + assertThat(ldap.userDn(), is(userDN)); + assertThat(groups, containsInAnyOrder( + containsString("Geniuses"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"))); + } } } public void testAuthenticateWithSAMAccountName() throws Exception { Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.ONE_LEVEL, false); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - //login with sAMAccountName - String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; - try (LdapSession ldap = session(sessionFactory, "selvig", SECURED_PASSWORD)) { - assertThat(ldap.userDn(), is(userDN)); + //login with sAMAccountName + String userDN = "CN=Erik Selvig,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"; + try (LdapSession ldap = session(sessionFactory, "selvig", SECURED_PASSWORD)) { + assertThat(ldap.userDn(), is(userDN)); - List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("Geniuses"), - containsString("CN=Users,CN=Builtin"), - containsString("Domain Users"))); + List groups = groups(ldap); + assertThat(groups, containsInAnyOrder( + containsString("Geniuses"), + containsString("CN=Users,CN=Builtin"), + containsString("Domain Users"))); + } } } @@ -224,16 +241,18 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put(ActiveDirectorySessionFactory.AD_USER_SEARCH_FILTER_SETTING, "(&(objectclass=user)(userPrincipalName={0}@ad.test.elasticsearch.com))") .build(); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - //Login with the UserPrincipalName - try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) { - List groups = groups(ldap); - assertThat(groups, containsInAnyOrder( - containsString("CN=Geniuses"), - containsString("CN=Domain Users"), - containsString("CN=Users,CN=Builtin"))); + //Login with the UserPrincipalName + try (LdapSession ldap = session(sessionFactory, "erik.selvig", SECURED_PASSWORD)) { + List groups = groups(ldap); + assertThat(groups, containsInAnyOrder( + containsString("CN=Geniuses"), + containsString("CN=Domain Users"), + containsString("CN=Users,CN=Builtin"))); + } } } @@ -256,7 +275,8 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put("ssl.truststore.password", "changeit") .build(); } - RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), + new ThreadContext(globalSettings)); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService); String user = "Bruce Banner"; @@ -290,7 +310,8 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put("ssl.truststore.password", "changeit") .build(); } - RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), + new ThreadContext(globalSettings)); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService); String user = "Bruce Banner"; @@ -318,7 +339,8 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put("ssl.truststore.password", "changeit") .build(); } - RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + RealmConfig config = new RealmConfig("ad-as-ldap-test", settings, globalSettings, new Environment(globalSettings), + new ThreadContext(globalSettings)); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService); String user = "Bruce Banner"; @@ -334,16 +356,19 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI } public void testAdAuthWithHostnameVerification() throws Exception { - RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, true), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); - ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService); + RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, true), globalSettings, + new Environment(globalSettings), new ThreadContext(globalSettings)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { - String userName = "ironman"; - UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class, - () -> session(sessionFactory, userName, SECURED_PASSWORD)); - assertThat(e.getCause(), instanceOf(ExecutionException.class)); - assertThat(e.getCause().getCause(), instanceOf(LDAPException.class)); - final LDAPException expected = (LDAPException) e.getCause().getCause(); - assertThat(expected.getMessage(), anyOf(containsString("Hostname verification failed"), containsString("peer not authenticated"))); + String userName = "ironman"; + UncategorizedExecutionException e = expectThrows(UncategorizedExecutionException.class, + () -> session(sessionFactory, userName, SECURED_PASSWORD)); + assertThat(e.getCause(), instanceOf(ExecutionException.class)); + assertThat(e.getCause().getCause(), instanceOf(LDAPException.class)); + final LDAPException expected = (LDAPException) e.getCause().getCause(); + assertThat(expected.getMessage(), + anyOf(containsString("Hostname verification failed"), containsString("peer not authenticated"))); + } } public void testStandardLdapHostnameVerification() throws Exception { @@ -353,7 +378,8 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI .put(LdapTestCase.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE)) .put("ssl.verification_mode", VerificationMode.FULL) .build(); - RealmConfig config = new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); + RealmConfig config = + new RealmConfig("ad-test", settings, globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); LdapSessionFactory sessionFactory = new LdapSessionFactory(config, sslService); String user = "Bruce Banner"; @@ -365,7 +391,30 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI assertThat(expected.getMessage(), anyOf(containsString("Hostname verification failed"), containsString("peer not authenticated"))); } - Settings buildAdSettings(String ldapUrl, String adDomainName, boolean hostnameVerification) { + public void testADLookup() throws Exception { + RealmConfig config = new RealmConfig("ad-test", + buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false, true), + globalSettings, new ThreadContext(Settings.EMPTY)); + try (ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, sslService)) { + + List users = randomSubsetOf(Arrays.asList("cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", + "cap@ad.test.elasticsearch.com", "hawkeye@ad.test.elasticsearch.com", "hulk@ad.test.elasticsearch.com", + "ironman@ad.test.elasticsearch.com", "thor@ad.test.elasticsearch.com", "blackwidow@ad.test.elasticsearch.com", + "ADES\\cap", "ADES\\hawkeye", "ADES\\hulk", "ADES\\ironman", "ADES\\thor", "ADES\\blackwidow")); + for (String user : users) { + try (LdapSession ldap = unauthenticatedSession(sessionFactory, user)) { + assertNotNull("ldap session was null for user " + user, ldap); + assertThat("group avenger test for user " + user, groups(ldap), hasItem(containsString("Avengers"))); + } + } + } + } + + private Settings buildAdSettings(String ldapUrl, String adDomainName, boolean hostnameVerification) { + return buildAdSettings(ldapUrl, adDomainName, hostnameVerification, randomBoolean()); + } + + private Settings buildAdSettings(String ldapUrl, String adDomainName, boolean hostnameVerification, boolean useBindUser) { Settings.Builder builder = Settings.builder() .put(ActiveDirectorySessionFactory.URLS_SETTING, ldapUrl) .put(ActiveDirectorySessionFactory.AD_DOMAIN_NAME_SETTING, adDomainName); @@ -374,10 +423,23 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI } else { builder.put(ActiveDirectorySessionFactory.HOSTNAME_VERIFICATION_SETTING, hostnameVerification); } + if (useGlobalSSL == false) { builder.put("ssl.truststore.path", getDataPath("../ldap/support/ldaptrust.jks")) .put("ssl.truststore.password", "changeit"); } + + if (useBindUser) { + final String user = randomFrom("cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", "cap@ad.test.elasticsearch.com", + "hawkeye@ad.test.elasticsearch.com", "hulk@ad.test.elasticsearch.com", "ironman@ad.test.elasticsearch.com", + "thor@ad.test.elasticsearch.com", "blackwidow@ad.test.elasticsearch.com", "ADES\\cap", "ADES\\hawkeye", "ADES\\hulk", + "ADES\\ironman", "ADES\\thor", "ADES\\blackwidow", "CN=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com"); + final boolean poolingEnabled = randomBoolean(); + builder.put("bind_dn", user) + .put("bind_password", PASSWORD) + .put("user_search.pool.enabled", poolingEnabled); + logger.info("using bind user [{}] with pooling enabled [{}]", user, poolingEnabled); + } return builder.build(); } @@ -387,6 +449,12 @@ public class ActiveDirectorySessionFactoryTests extends AbstractActiveDirectoryI return future.actionGet(); } + private LdapSession unauthenticatedSession(SessionFactory factory, String username) { + PlainActionFuture future = new PlainActionFuture<>(); + factory.unauthenticatedSession(username, future); + return future.actionGet(); + } + private List groups(LdapSession ldapSession) { PlainActionFuture> future = new PlainActionFuture<>(); ldapSession.groups(future); diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java index 787cae5e295..83037e44d49 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapRealmTests.java @@ -230,7 +230,7 @@ public class LdapRealmTests extends LdapTestCase { try { assertThat(sessionFactory, is(instanceOf(LdapUserSearchSessionFactory.class))); } finally { - ((LdapUserSearchSessionFactory)sessionFactory).shutdown(); + ((LdapUserSearchSessionFactory)sessionFactory).close(); } } diff --git a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java index e9bfbf68690..2b52e6f0f0c 100644 --- a/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java +++ b/plugin/src/test/java/org/elasticsearch/xpack/security/authc/ldap/LdapUserSearchSessionFactoryTests.java @@ -95,7 +95,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { try { assertThat(sessionFactory.supportsUnauthenticatedSession(), is(true)); } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -140,7 +140,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertThat(dn, containsString(user)); } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -177,7 +177,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertNull(session(sessionFactory, user, userPass)); assertNull(unauthenticatedSession(sessionFactory, user)); } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -223,7 +223,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertThat(dn, containsString(user)); } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -260,7 +260,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertNull(session(sessionFactory, user, userPass)); assertNull(unauthenticatedSession(sessionFactory, user)); } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -306,7 +306,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertThat(dn, containsString(user)); } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -342,7 +342,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertNull(session(sessionFactory, user, userPass)); assertNull(unauthenticatedSession(sessionFactory, user)); } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } if (useAttribute) { @@ -380,7 +380,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { assertThat(dn, containsString("William Bush")); } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } } @@ -434,7 +434,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { containsString("Philanthropists"))); } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } } @@ -478,7 +478,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { } } } finally { - sessionFactory.shutdown(); + sessionFactory.close(); } } @@ -493,7 +493,9 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { .build(), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); LDAPConnectionPool connectionPool = LdapUserSearchSessionFactory.createConnectionPool(config, new SingleServerSet("localhost", - randomFrom(ldapServers).getListenPort()), TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE); + randomFrom(ldapServers).getListenPort()), TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE, + () -> new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass"), + () -> "cn=Horatio Hornblower,ou=people,o=sevenSeas"); try { assertThat(connectionPool.getCurrentAvailableConnections(), is(LdapUserSearchSessionFactory.DEFAULT_CONNECTION_POOL_INITIAL_SIZE)); @@ -522,7 +524,9 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { .build(), globalSettings, new Environment(globalSettings), new ThreadContext(globalSettings)); LDAPConnectionPool connectionPool = LdapUserSearchSessionFactory.createConnectionPool(config, new SingleServerSet("localhost", - randomFrom(ldapServers).getListenPort()), TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE); + randomFrom(ldapServers).getListenPort()), TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE, + () -> new SimpleBindRequest("cn=Horatio Hornblower,ou=people,o=sevenSeas", "pass"), + () -> "cn=Horatio Hornblower,ou=people,o=sevenSeas"); try { assertThat(connectionPool.getCurrentAvailableConnections(), is(10)); assertThat(connectionPool.getMaximumAvailableConnections(), is(12)); @@ -547,7 +551,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { searchSessionFactory = new LdapUserSearchSessionFactory(config, sslService); } finally { if (searchSessionFactory != null) { - searchSessionFactory.shutdown(); + searchSessionFactory.close(); } } } @@ -570,7 +574,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { future.get(); } finally { if (searchSessionFactory != null) { - searchSessionFactory.shutdown(); + searchSessionFactory.close(); } } } @@ -619,7 +623,7 @@ public class LdapUserSearchSessionFactoryTests extends LdapTestCase { searchSessionFactory = new LdapUserSearchSessionFactory(config, sslService); } finally { if (searchSessionFactory != null) { - searchSessionFactory.shutdown(); + searchSessionFactory.close(); } }