Add active directory bind user and user lookup support (elastic/x-pack-elasticsearch#1956)

This commit adds support for a bind user when using the active directory realm. The addition of a
bind user also enables support for the user lookup mechanism, which is necessary to support the run
as functionality that we provide.

relates elastic/x-pack-elasticsearch#179

Original commit: elastic/x-pack-elasticsearch@40b07b3422
This commit is contained in:
Jay Modi 2017-07-12 14:01:39 -06:00 committed by GitHub
parent ef25568b2a
commit e686d8a3bf
12 changed files with 767 additions and 372 deletions

View File

@ -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:<domain_name>: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 <<run-as-privilege,run as feature>> 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://<domain_name>: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 <<ad-load-balancing>>.
| `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.

View File

@ -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
<<ldap-user-search, Configuring an LDAP Realm with User Search>>.
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
<<ldap-user-search, _user search_ mode>>. The Active Directory realm must be
<<ad-settings,configured with a `bind_dn` and `bind_password`>> 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

View File

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

View File

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

View File

@ -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<Boolean> 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<LdapSession> listener) {
void getSessionWithPool(LDAPConnectionPool connectionPool, String user, SecureString password, ActionListener<LdapSession> listener) {
getADAuthenticator(user).authenticate(connectionPool, user, password, listener);
}
@Override
void getSessionWithoutPool(String username, SecureString password, ActionListener<LdapSession> 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<LdapSession> 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<LdapSession> 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<Setting<?>> getSettings() {
Set<Setting<?>> 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<LdapSession> 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<LdapSession> 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<SearchResultEntry> 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<SearchResultEntry> 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<SearchResultEntry> 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<String> 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<SearchResultEntry> listener) {
String[] parts = username.split("@");
assert parts.length == 2;

View File

@ -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<String> SEARCH_ATTRIBUTE = new Setting<>("user_search.attribute", DEFAULT_USERNAME_ATTRIBUTE,
@ -59,36 +49,22 @@ class LdapUserSearchSessionFactory extends SessionFactory {
private static final Setting<String> SEARCH_FILTER = Setting.simpleString("user_search.filter", Setting.Property.NodeScope);
private static final Setting<LdapSearchScope> SEARCH_SCOPE = new Setting<>("user_search.scope", (String) null,
s -> LdapSearchScope.resolve(s, LdapSearchScope.SUB_TREE), Setting.Property.NodeScope);
private static final Setting<Boolean> POOL_ENABLED = Setting.boolSetting("user_search.pool.enabled",
true, Setting.Property.NodeScope);
private static final Setting<Integer> POOL_INITIAL_SIZE = Setting.intSetting("user_search.pool.initial_size",
DEFAULT_CONNECTION_POOL_INITIAL_SIZE, 0, Setting.Property.NodeScope);
private static final Setting<Integer> POOL_SIZE = Setting.intSetting("user_search.pool.size",
DEFAULT_CONNECTION_POOL_SIZE, 1, Setting.Property.NodeScope);
private static final Setting<TimeValue> HEALTH_CHECK_INTERVAL = Setting.timeSetting("user_search.pool.health_check.interval",
DEFAULT_HEALTH_CHECK_INTERVAL, Setting.Property.NodeScope);
private static final Setting<Boolean> HEALTH_CHECK_ENABLED = Setting.boolSetting("user_search.pool.health_check.enabled",
true, Setting.Property.NodeScope);
private static final Setting<Optional<String>> HEALTH_CHECK_DN = new Setting<>("user_search.pool.health_check.dn", (String) null,
Optional::ofNullable, Setting.Property.NodeScope);
private static final Setting<String> BIND_DN = Setting.simpleString("bind_dn",
Setting.Property.NodeScope, Setting.Property.Filtered);
private static final Setting<String> BIND_PASSWORD = Setting.simpleString("bind_password",
Setting.Property.NodeScope, Setting.Property.Filtered);
private static final Setting<Boolean> 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<LdapSession> 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<LdapSession> listener) {
@Override
void getSessionWithPool(LDAPConnectionPool connectionPool, String user, SecureString password, ActionListener<LdapSession> listener) {
findUser(user, connectionPool, ActionListener.wrap((entry) -> {
if (entry == null) {
listener.onResponse(null);
@ -204,7 +125,8 @@ class LdapUserSearchSessionFactory extends SessionFactory {
* <li>Creates a new LDAPSession with the bound connection</li>
* </ol>
*/
private void getSessionWithoutPool(String user, SecureString password, ActionListener<LdapSession> listener) {
@Override
void getSessionWithoutPool(String user, SecureString password, ActionListener<LdapSession> listener) {
boolean success = false;
LDAPConnection connection = null;
try {
@ -261,33 +183,42 @@ class LdapUserSearchSessionFactory extends SessionFactory {
}
@Override
public void unauthenticatedSession(String user, ActionListener<LdapSession> listener) {
void getUnauthenticatedSessionWithPool(LDAPConnectionPool connectionPool, String user, ActionListener<LdapSession> 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<LdapSession> 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<Setting<?>> getSettings() {
Set<Setting<?>> 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());

View File

@ -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<String> BIND_DN = Setting.simpleString("bind_dn", Setting.Property.NodeScope, Setting.Property.Filtered);
static final Setting<String> 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<Integer> POOL_INITIAL_SIZE = Setting.intSetting("user_search.pool.initial_size",
DEFAULT_CONNECTION_POOL_INITIAL_SIZE, 0, Setting.Property.NodeScope);
private static final Setting<Integer> POOL_SIZE = Setting.intSetting("user_search.pool.size",
DEFAULT_CONNECTION_POOL_SIZE, 1, Setting.Property.NodeScope);
private static final Setting<TimeValue> HEALTH_CHECK_INTERVAL = Setting.timeSetting("user_search.pool.health_check.interval",
DEFAULT_HEALTH_CHECK_INTERVAL, Setting.Property.NodeScope);
private static final Setting<Boolean> HEALTH_CHECK_ENABLED = Setting.boolSetting("user_search.pool.health_check.enabled",
true, Setting.Property.NodeScope);
private static final Setting<Optional<String>> 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<Boolean> poolingEnabled, Supplier<BindRequest> bindRequestSupplier,
Supplier<String> 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<LdapSession> listener) {
if (useConnectionPool) {
getSessionWithPool(connectionPool, user, password, listener);
} else {
getSessionWithoutPool(user, password, listener);
}
}
@Override
public final void unauthenticatedSession(String user, ActionListener<LdapSession> 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<LdapSession> 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<LdapSession> 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<LdapSession> 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<LdapSession> 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<BindRequest> bindRequestSupplier,
Supplier<String> 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<Setting<?>> getSettings() {
return Sets.newHashSet(POOL_INITIAL_SIZE, POOL_SIZE, HEALTH_CHECK_ENABLED, HEALTH_CHECK_INTERVAL, HEALTH_CHECK_DN, BIND_DN,
BIND_PASSWORD);
}
}

View File

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

View File

@ -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<String> 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<String> 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<String> groups = resolveBlocking(resolver, ldapConnection, BRUCE_BANNER_DN,
TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE, null);
assertThat(groups, hasItem(containsString("CN=Users,CN=Builtin")));

View File

@ -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<String> 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<String> 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<String> 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<String> 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<List<String>> 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<List<String>> 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<String> groups = groups(ldap);
String userName = "hulk";
try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
List<String> 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<String> groups = groups(ldap);
String userName = "hulk";
try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
List<String> 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<String> groups = groups(ldap);
String userName = "hulk";
try (LdapSession ldap = session(sessionFactory, userName, SECURED_PASSWORD)) {
List<String> 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<String> 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<String> 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<String> groups = groups(ldap);
assertThat(groups, containsInAnyOrder(
containsString("Geniuses"),
containsString("CN=Users,CN=Builtin"),
containsString("Domain Users")));
List<String> 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<String> 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<String> 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<String> 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<LdapSession> future = new PlainActionFuture<>();
factory.unauthenticatedSession(username, future);
return future.actionGet();
}
private List<String> groups(LdapSession ldapSession) {
PlainActionFuture<List<String>> future = new PlainActionFuture<>();
ldapSession.groups(future);

View File

@ -230,7 +230,7 @@ public class LdapRealmTests extends LdapTestCase {
try {
assertThat(sessionFactory, is(instanceOf(LdapUserSearchSessionFactory.class)));
} finally {
((LdapUserSearchSessionFactory)sessionFactory).shutdown();
((LdapUserSearchSessionFactory)sessionFactory).close();
}
}

View File

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