[ldap] Migrate to using UnboundID SDK for LDAP

This migrates all of the LDAP code off of JNDI and makes use of the UnboundID
SDK to perform LDAP communication. As much as possible the behavior has
remained consistent. The minimum ldap search timeout is now 1s as UnboundID
only accepts this timeout in seconds; previously a value in milliseconds could be
specified.

Closes elastic/elasticsearch#694

Original commit: elastic/x-pack-elasticsearch@dd1c92bf91
This commit is contained in:
jaymode 2015-02-06 08:24:41 -05:00
parent 17e16e2c53
commit 4de8d04f9f
55 changed files with 1348 additions and 2001 deletions

View File

@ -58,6 +58,3 @@ java.nio.channels.ReadableByteChannel#read(java.nio.ByteBuffer)
java.nio.channels.ScatteringByteChannel#read(java.nio.ByteBuffer[])
java.nio.channels.ScatteringByteChannel#read(java.nio.ByteBuffer[], int, int)
java.nio.channels.FileChannel#read(java.nio.ByteBuffer, long)
@defaultMessage The LdapSslSocketFactory should never be cleared manually as it may lead to threading issues.
org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory#clear()

13
pom.xml
View File

@ -117,9 +117,9 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-all</artifactId>
<version>2.0.0-M17</version>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
@ -177,13 +177,16 @@
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>dk.brics.automaton</groupId>
<artifactId>automaton</artifactId>
<version>1.11-8</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>2.3.8</version>
</dependency>
</dependencies>

View File

@ -25,6 +25,7 @@
<include>org.elasticsearch:elasticsearch-shield</include>
<include>commons-codec:commons-codec</include>
<include>dk.brics.automaton:automaton</include>
<include>com.unboundid:unboundid-ldapsdk</include>
</includes>
</dependencySet>
</dependencySets>

View File

@ -7,10 +7,9 @@ package org.elasticsearch.shield.authc;
import org.elasticsearch.common.inject.multibindings.MapBinder;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.esusers.ESUsersRealm;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapSslSocketFactory;
import org.elasticsearch.shield.support.AbstractShieldModule;
/**
@ -24,11 +23,6 @@ public class AuthenticationModule extends AbstractShieldModule.Node {
@Override
protected void configureNode() {
// This socket factory needs to be configured before any LDAP connections are created. LDAP configuration
// for JNDI invokes a static getSocketFactory method from LdapSslSocketFactory.
requestStaticInjection(AbstractLdapSslSocketFactory.class);
MapBinder<String, Realm.Factory> mapBinder = MapBinder.newMapBinder(binder(), String.class, Realm.Factory.class);
mapBinder.addBinding(ESUsersRealm.TYPE).to(ESUsersRealm.Factory.class).asEagerSingleton();
mapBinder.addBinding(ActiveDirectoryRealm.TYPE).to(ActiveDirectoryRealm.Factory.class).asEagerSingleton();

View File

@ -1,26 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import javax.naming.directory.DirContext;
/**
* An Ldap Connection customized for active directory.
*/
public class ActiveDirectoryConnection extends AbstractLdapConnection {
/**
* This object is intended to be constructed by the LdapConnectionFactory
*/
ActiveDirectoryConnection(ESLogger logger, DirContext ctx, String boundName, GroupsResolver resolver, TimeValue timeout) {
super(logger, ctx, boundName, resolver, timeout);
}
}

View File

@ -1,137 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.io.Serializable;
import java.util.Hashtable;
/**
* This Class creates LdapConnections authenticating via the custom Active Directory protocol. (that being
* authenticating with a principal name, "username@domain", then searching through the directory to find the
* 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.
*/
public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDirectoryConnection> {
public static final String AD_DOMAIN_NAME_SETTING = "domain_name";
public static final String AD_GROUP_SEARCH_BASEDN_SETTING = "group_search.base_dn";
public static final String AD_GROUP_SEARCH_SCOPE_SETTING = "group_search.scope";
public static final String AD_USER_SEARCH_BASEDN_SETTING = "user_search.base_dn";
public static final String AD_USER_SEARCH_FILTER_SETTING = "user_search.filter";
public static final String AD_USER_SEARCH_SCOPE_SETTING = "user_search.scope";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String userSearchDN;
private final String domainName;
private final String userSearchFilter;
private final SearchScope userSearchScope;
private final TimeValue timeout;
private final AbstractLdapConnection.GroupsResolver groupResolver;
public ActiveDirectoryConnectionFactory(RealmConfig config) {
super(ActiveDirectoryConnection.class, config);
Settings settings = config.settings();
domainName = settings.get(AD_DOMAIN_NAME_SETTING);
if (domainName == null) {
throw new ShieldSettingsException("missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory");
}
String domainDN = buildDnFromDomain(domainName);
userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, domainDN);
userSearchScope = SearchScope.resolve(settings.get(AD_USER_SEARCH_SCOPE_SETTING), SearchScope.SUB_TREE);
userSearchFilter = settings.get(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0}@" + domainName + ")))");
timeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
String[] ldapUrls = settings.getAsArray(URLS_SETTING, new String[] { "ldap://" + domainName + ":389" });
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
.put(JNDI_LDAP_CONNECT_TIMEOUT, Long.toString(settings.getAsTime(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT).millis()))
.put(JNDI_LDAP_READ_TIMEOUT, Long.toString(settings.getAsTime(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT).millis()))
.put("java.naming.ldap.attributes.binary", "tokenGroups")
.put(Context.REFERRAL, "follow");
configureJndiSSL(ldapUrls, builder);
sharedLdapEnv = builder.build();
groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN);
}
/**
* 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
* @return An authenticated
*/
@Override
public ActiveDirectoryConnection open(String userName, SecuredString password) {
String userPrincipal = userName + "@" + domainName;
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, userPrincipal);
ldapEnv.put(Context.SECURITY_CREDENTIALS, password.internalChars());
DirContext ctx = null;
try {
ctx = new InitialDirContext(ldapEnv);
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(userSearchScope.scope());
searchCtls.setReturningAttributes(Strings.EMPTY_ARRAY);
searchCtls.setTimeLimit((int) timeout.millis());
try (ClosableNamingEnumeration<SearchResult> results = new ClosableNamingEnumeration<>(
ctx.search(userSearchDN, userSearchFilter, new Object[] { userName }, searchCtls))) {
if(results.hasMore()){
SearchResult entry = results.next();
String name = entry.getNameInNamespace();
if (!results.hasMore()) {
return new ActiveDirectoryConnection(connectionLogger, ctx, name, groupResolver, timeout);
}
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded multiple results");
} else {
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded no results");
}
}
} catch (Throwable e) {
if (ctx != null) {
try {
ctx.close();
} catch (NamingException ne) {
logger.trace("an unexpected error occurred closing an LDAP context", ne);
}
}
throw new ActiveDirectoryException("unable to authenticate user [" + userName + "] to active directory domain [" + domainName + "]", e);
}
}
/**
* @param domain active directory domain name
* @return LDAP DN, distinguished name, of the root of the domain
*/
String buildDnFromDomain(String domain) {
return "DC=" + domain.replace(".", ",DC=");
}
}

View File

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
import org.elasticsearch.watcher.ResourceWatcherService;
/**
* LDAP Group to role mapper specific to the "shield.authc.ldap" package
*/
public class ActiveDirectoryGroupToRoleMapper extends AbstractGroupToRoleMapper {
public ActiveDirectoryGroupToRoleMapper(RealmConfig config, ResourceWatcherService watcherService) {
super(ActiveDirectoryRealm.TYPE, config, watcherService, null);
}
}

View File

@ -1,151 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.ldap.LdapException;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.List;
/**
*
*/
public class ActiveDirectoryGroupsResolver implements AbstractLdapConnection.GroupsResolver {
private final String baseDn;
private final SearchScope scope;
public ActiveDirectoryGroupsResolver(Settings settings, String baseDnDefault) {
this.baseDn = settings.get("base_dn", baseDnDefault);
this.scope = SearchScope.resolve(settings.get("scope"), SearchScope.SUB_TREE);
}
public List<String> resolve(DirContext ctx, String userDn, TimeValue timeout, ESLogger logger) {
String groupsSearchFilter = buildGroupQuery(ctx, userDn, timeout);
logger.debug("group SID to DN search filter: [{}]", groupsSearchFilter);
// Search for groups the user belongs to in order to get their names
//Create the search controls
SearchControls groupsSearchCtls = new SearchControls();
//Specify the search scope
groupsSearchCtls.setSearchScope(scope.scope());
groupsSearchCtls.setReturningAttributes(Strings.EMPTY_ARRAY); //we only need the entry DN
groupsSearchCtls.setTimeLimit((int) timeout.millis());
ImmutableList.Builder<String> groups = ImmutableList.builder();
try (ClosableNamingEnumeration<SearchResult> groupsAnswer = new ClosableNamingEnumeration<>(
ctx.search(baseDn, groupsSearchFilter, groupsSearchCtls))) {
//Loop through the search results
while (groupsAnswer.hasMoreElements()) {
SearchResult sr = groupsAnswer.next();
groups.add(sr.getNameInNamespace());
}
} catch (NamingException | LdapException ne) {
throw new ActiveDirectoryException("failed to fetch AD groups", userDn, ne);
}
List<String> groupList = groups.build();
if (logger.isDebugEnabled()) {
logger.debug("found these groups [{}] for userDN [{}]", groupList, userDn);
}
return groupList;
}
static String buildGroupQuery(DirContext ctx, String userDn, TimeValue timeout) {
StringBuilder groupsSearchFilter = new StringBuilder("(|");
try {
SearchControls userSearchCtls = new SearchControls();
userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
userSearchCtls.setTimeLimit((int) timeout.millis());
userSearchCtls.setReturningAttributes(new String[] { "tokenGroups" });
try (ClosableNamingEnumeration<SearchResult> userAnswer = new ClosableNamingEnumeration<>(
ctx.search(userDn, "(objectClass=user)", userSearchCtls))) {
while (userAnswer.hasMoreElements()) {
SearchResult sr = userAnswer.next();
Attributes attrs = sr.getAttributes();
if (attrs != null) {
try (ClosableNamingEnumeration<? extends Attribute> ae = new ClosableNamingEnumeration<>(attrs.getAll())) {
while (ae.hasMore() ) {
Attribute attr = ae.next();
for (NamingEnumeration e = attr.getAll(); e.hasMore(); ) {
byte[] sid = (byte[]) e.next();
groupsSearchFilter.append("(objectSid=");
groupsSearchFilter.append(binarySidToStringSid(sid));
groupsSearchFilter.append(")");
}
groupsSearchFilter.append(")");
}
}
}
}
}
} catch (NamingException | LdapException ne) {
throw new ActiveDirectoryException("failed to fetch AD groups", userDn, ne);
}
return groupsSearchFilter.toString();
}
/**
* To better understand what the sid is and how its string representation looks like, see
* http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx
*
* @param SID byte encoded security ID
*/
static public String binarySidToStringSid(byte[] SID) {
String strSID;
//convert the SID into string format
long version;
long authority;
long count;
long rid;
strSID = "S";
version = SID[0];
strSID = strSID + "-" + Long.toString(version);
authority = SID[4];
for (int i = 0; i < 4; i++) {
authority <<= 8;
authority += SID[4 + i] & 0xFF;
}
strSID = strSID + "-" + Long.toString(authority);
count = SID[2];
count <<= 8;
count += SID[1] & 0xFF;
for (int j = 0; j < count; j++) {
rid = SID[11 + (j * 4)] & 0xFF;
for (int k = 1; k < 4; k++) {
rid <<= 8;
rid += SID[11 - k + (j * 4)] & 0xFF;
}
strSID = strSID + "-" + Long.toString(rid);
}
return strSID;
}
}

View File

@ -3,12 +3,12 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
package org.elasticsearch.shield.authc.activedirectory;
import org.elasticsearch.shield.ShieldException;
/**
* ActiveDirectoryExceptions typically wrap jndi Naming exceptions, and have an additional
* ActiveDirectoryExceptions typically wrap {@link com.unboundid.ldap.sdk.LDAPException}, and have an additional
* parameter of DN attached to each message.
*/
public class ActiveDirectoryException extends ShieldException {

View File

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.activedirectory;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import java.util.ArrayList;
import java.util.List;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.OBJECT_CLASS_PRESENCE_FILTER;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.search;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.searchForEntry;
/**
*
*/
public class ActiveDirectoryGroupsResolver implements GroupsResolver {
private final String baseDn;
private final LdapSearchScope scope;
public ActiveDirectoryGroupsResolver(Settings settings, String baseDnDefault) {
this.baseDn = settings.get("base_dn", baseDnDefault);
this.scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE);
}
public List<String> resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) {
Filter groupSearchFilter = buildGroupQuery(connection, userDn, timeout, logger);
logger.debug("group SID to DN search filter: [{}]", groupSearchFilter);
SearchRequest searchRequest = new SearchRequest(baseDn, scope.scope(), groupSearchFilter, Strings.EMPTY_ARRAY);
searchRequest.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResult results;
try {
results = search(connection, searchRequest, logger);
} catch (LDAPException e) {
throw new ActiveDirectoryException("failed to fetch AD groups", userDn, e);
}
ImmutableList.Builder<String> groups = ImmutableList.builder();
for (SearchResultEntry entry : results.getSearchEntries()) {
groups.add(entry.getDN());
}
List<String> groupList = groups.build();
if (logger.isDebugEnabled()) {
logger.debug("found these groups [{}] for userDN [{}]", groupList, userDn);
}
return groupList;
}
static Filter buildGroupQuery(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) {
try {
SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, "tokenGroups");
request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResultEntry entry = searchForEntry(connection, request, logger);
Attribute attribute = entry.getAttribute("tokenGroups");
byte[][] tokenGroupSIDBytes = attribute.getValueByteArrays();
List<Filter> orFilters = new ArrayList<>(tokenGroupSIDBytes.length);
for (byte[] SID : tokenGroupSIDBytes) {
orFilters.add(Filter.createEqualityFilter("objectSid", binarySidToStringSid(SID)));
}
return Filter.createORFilter(orFilters);
} catch (LDAPException e) {
throw new ActiveDirectoryException("failed to fetch AD groups", userDn, e);
}
}
/**
* To better understand what the sid is and how its string representation looks like, see
* http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx
*
* @param SID byte encoded security ID
*/
static public String binarySidToStringSid(byte[] SID) {
String strSID;
//convert the SID into string format
long version;
long authority;
long count;
long rid;
strSID = "S";
version = SID[0];
strSID = strSID + "-" + Long.toString(version);
authority = SID[4];
for (int i = 0; i < 4; i++) {
authority <<= 8;
authority += SID[4 + i] & 0xFF;
}
strSID = strSID + "-" + Long.toString(authority);
count = SID[2];
count <<= 8;
count += SID[1] & 0xFF;
for (int j = 0; j < count; j++) {
rid = SID[11 + (j * 4)] & 0xFF;
for (int k = 1; k < 4; k++) {
rid <<= 8;
rid += SID[11 - k + (j * 4)] & 0xFF;
}
strSID = strSID + "-" + Long.toString(rid);
}
return strSID;
}
}

View File

@ -3,12 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
package org.elasticsearch.shield.authc.activedirectory;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapRealm;
import org.elasticsearch.shield.authc.ldap.support.AbstractLdapRealm;
import org.elasticsearch.shield.authc.ldap.support.GroupToRoleMapper;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.watcher.ResourceWatcherService;
/**
@ -19,8 +21,8 @@ public class ActiveDirectoryRealm extends AbstractLdapRealm {
public static final String TYPE = "active_directory";
public ActiveDirectoryRealm(RealmConfig config,
ActiveDirectoryConnectionFactory connectionFactory,
ActiveDirectoryGroupToRoleMapper roleMapper) {
ActiveDirectorySessionFactory connectionFactory,
GroupToRoleMapper roleMapper) {
super(TYPE, config, connectionFactory, roleMapper);
}
@ -28,17 +30,19 @@ public class ActiveDirectoryRealm extends AbstractLdapRealm {
public static class Factory extends AbstractLdapRealm.Factory<ActiveDirectoryRealm> {
private final ResourceWatcherService watcherService;
private final ClientSSLService clientSSLService;
@Inject
public Factory(ResourceWatcherService watcherService, RestController restController) {
public Factory(ResourceWatcherService watcherService, RestController restController, ClientSSLService clientSSLService) {
super(ActiveDirectoryRealm.TYPE, restController);
this.watcherService = watcherService;
this.clientSSLService = clientSSLService;
}
@Override
public ActiveDirectoryRealm create(RealmConfig config) {
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectoryGroupToRoleMapper roleMapper = new ActiveDirectoryGroupToRoleMapper(config, watcherService);
ActiveDirectorySessionFactory connectionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
GroupToRoleMapper roleMapper = new GroupToRoleMapper(TYPE, config, watcherService, null);
return new ActiveDirectoryRealm(config, connectionFactory, roleMapper);
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.activedirectory;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.ldap.support.LdapSession;
import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.ssl.ClientSSLService;
import javax.net.SocketFactory;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.*;
/**
* This Class creates LdapSessions authenticating via the custom Active Directory protocol. (that being
* authenticating with a principal name, "username@domain", then searching through the directory to find the
* 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.
*/
public class ActiveDirectorySessionFactory extends SessionFactory {
public static final String AD_DOMAIN_NAME_SETTING = "domain_name";
public static final String AD_GROUP_SEARCH_BASEDN_SETTING = "group_search.base_dn";
public static final String AD_GROUP_SEARCH_SCOPE_SETTING = "group_search.scope";
public static final String AD_USER_SEARCH_BASEDN_SETTING = "user_search.base_dn";
public static final String AD_USER_SEARCH_FILTER_SETTING = "user_search.filter";
public static final String AD_USER_SEARCH_SCOPE_SETTING = "user_search.scope";
private final String userSearchDN;
private final String domainName;
private final String userSearchFilter;
private final LdapSearchScope userSearchScope;
private final GroupsResolver groupResolver;
private final ServerSet ldapServerSet;
public ActiveDirectorySessionFactory(RealmConfig config, ClientSSLService sslService) {
super(config);
Settings settings = config.settings();
domainName = settings.get(AD_DOMAIN_NAME_SETTING);
if (domainName == null) {
throw new ShieldSettingsException("missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory");
}
String domainDN = buildDnFromDomain(domainName);
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(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0}@" + domainName + ")))");
ldapServerSet = serverSet(config.settings(), sslService);
groupResolver = new ActiveDirectoryGroupsResolver(settings.getAsSettings("group_search"), domainDN);
}
ServerSet serverSet(Settings settings, ClientSSLService clientSSLService) {
String[] ldapUrls = settings.getAsArray(URLS_SETTING, new String[] { "ldap://" + domainName + ":389" });
LDAPServers servers = new LDAPServers(ldapUrls);
LDAPConnectionOptions options = connectionOptions(settings);
SocketFactory socketFactory;
if (servers.ssl()) {
socketFactory = clientSSLService.sslSocketFactory();
if (settings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
logger.debug("using encryption for LDAP connections with hostname verification");
} else {
logger.debug("using encryption for LDAP connections without hostname verification");
}
} else {
socketFactory = null;
}
FailoverServerSet serverSet = new FailoverServerSet(servers.addresses(), servers.ports(), socketFactory, options);
serverSet.setReOrderOnFailover(true);
return serverSet;
}
/**
* 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
* @return An authenticated
*/
@Override
public LdapSession open(String userName, SecuredString password) {
LDAPConnection connection;
try {
connection = ldapServerSet.getConnection();
} catch (LDAPException e) {
throw new ActiveDirectoryException("failed to connect to any active directory servers");
}
String userPrincipal = userName + "@" + domainName;
try {
connection.bind(userPrincipal, new String(password.internalChars()));
SearchRequest searchRequest = new SearchRequest(userSearchDN, userSearchScope.scope(), createFilter(userSearchFilter, userName), Strings.EMPTY_ARRAY);
searchRequest.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResult results = search(connection, searchRequest, logger);
int numResults = results.getEntryCount();
if (numResults > 1) {
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded multiple results");
} else if (numResults < 1) {
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded no results");
}
String dn = results.getSearchEntries().get(0).getDN();
return new LdapSession(connectionLogger, connection, dn, groupResolver, timeout);
} catch (LDAPException e) {
connection.close();
throw new ActiveDirectoryException("unable to authenticate user [" + userName + "] to active directory domain [" + domainName + "]", e);
}
}
/**
* @param domain active directory domain name
* @return LDAP DN, distinguished name, of the root of the domain
*/
String buildDnFromDomain(String domain) {
return "DC=" + domain.replace(".", ",DC=");
}
}

View File

@ -1,33 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import javax.naming.directory.DirContext;
/**
* Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming
* instances of this connection will be produced by the LdapConnectionFactory.open() methods.
* <p/>
* A standard looking usage pattern could look like this:
* <pre>
* try (LdapConnection session = ldapFac.bindXXX(...);
* ...do stuff with the session
* }
* </pre>
*/
public class LdapConnection extends AbstractLdapConnection {
/**
* This object is intended to be constructed by the LdapConnectionFactory
*/
LdapConnection(ESLogger logger, DirContext ctx, String bindDN, AbstractLdapConnection.GroupsResolver groupsResolver, TimeValue timeout) {
super(logger, ctx, bindDN, groupsResolver, timeout);
}
}

View File

@ -1,21 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
import org.elasticsearch.watcher.ResourceWatcherService;
/**
* LDAP Group to role mapper specific to the "shield.authc.ldap" package
*/
public class LdapGroupToRoleMapper extends AbstractGroupToRoleMapper {
public LdapGroupToRoleMapper(RealmConfig config, ResourceWatcherService watcherService) {
super(LdapRealm.TYPE, config, watcherService, null);
}
}

View File

@ -8,7 +8,9 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapRealm;
import org.elasticsearch.shield.authc.ldap.support.AbstractLdapRealm;
import org.elasticsearch.shield.authc.ldap.support.GroupToRoleMapper;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.watcher.ResourceWatcherService;
/**
@ -18,25 +20,27 @@ public class LdapRealm extends AbstractLdapRealm {
public static final String TYPE = "ldap";
public LdapRealm(RealmConfig config, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper) {
public LdapRealm(RealmConfig config, LdapSessionFactory ldap, GroupToRoleMapper roleMapper) {
super(TYPE, config, ldap, roleMapper);
}
public static class Factory extends AbstractLdapRealm.Factory<LdapRealm> {
private final ResourceWatcherService watcherService;
private final ClientSSLService clientSSLService;
@Inject
public Factory(ResourceWatcherService watcherService, RestController restController) {
public Factory(ResourceWatcherService watcherService, RestController restController, ClientSSLService clientSSLService) {
super(TYPE, restController);
this.watcherService = watcherService;
this.clientSSLService = clientSSLService;
}
@Override
public LdapRealm create(RealmConfig config) {
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapGroupToRoleMapper roleMapper = new LdapGroupToRoleMapper(config, watcherService);
return new LdapRealm(config, connectionFactory, roleMapper);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
GroupToRoleMapper roleMapper = new GroupToRoleMapper(TYPE, config, watcherService, null);
return new LdapRealm(config, sessionFactory, roleMapper);
}
}
}

View File

@ -5,24 +5,20 @@
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.ldap.support.LdapSession;
import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.ssl.ClientSSLService;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.ldap.Rdn;
import java.io.Serializable;
import javax.net.SocketFactory;
import java.text.MessageFormat;
import java.util.Hashtable;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.escapedRDNValue;
/**
* This factory creates LDAP connections via iterating through user templates.
@ -30,71 +26,81 @@ import java.util.Hashtable;
* Note that even though there is a separate factory for Active Directory, this factory would work against AD. A template
* for each user context would need to be supplied.
*/
public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
public class LdapSessionFactory extends SessionFactory {
public static final String USER_DN_TEMPLATES_SETTING = "user_dn_templates";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String[] userDnTemplates;
private final AbstractLdapConnection.GroupsResolver groupResolver;
private final TimeValue timeout;
private final GroupsResolver groupResolver;
private final ServerSet ldapServerSet;
public LdapConnectionFactory(RealmConfig config) {
super(LdapConnection.class, config);
public LdapSessionFactory(RealmConfig config, ClientSSLService sslService) {
super(config);
Settings settings = config.settings();
userDnTemplates = settings.getAsArray(USER_DN_TEMPLATES_SETTING);
if (userDnTemplates == null) {
throw new ShieldSettingsException("missing required LDAP setting [" + USER_DN_TEMPLATES_SETTING + "]");
}
this.ldapServerSet = serverSet(config.settings(), sslService);
groupResolver = groupResolver(settings);
}
ServerSet serverSet(Settings settings, ClientSSLService clientSSLService) {
// Parse LDAP urls
String[] ldapUrls = settings.getAsArray(URLS_SETTING);
if (ldapUrls == null || ldapUrls.length == 0) {
throw new ShieldSettingsException("missing required LDAP setting [" + URLS_SETTING + "]");
}
timeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
.put(JNDI_LDAP_READ_TIMEOUT, Long.toString(settings.getAsTime(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT).millis()))
.put(JNDI_LDAP_CONNECT_TIMEOUT, Long.toString(settings.getAsTime(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT).millis()))
.put(Context.REFERRAL, "follow");
configureJndiSSL(ldapUrls, builder);
sharedLdapEnv = builder.build();
groupResolver = groupResolver(settings);
LDAPServers servers = new LDAPServers(ldapUrls);
LDAPConnectionOptions options = connectionOptions(settings);
SocketFactory socketFactory;
if (servers.ssl()) {
socketFactory = clientSSLService.sslSocketFactory();
if (settings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
logger.debug("using encryption for LDAP connections with hostname verification");
} else {
logger.debug("using encryption for LDAP connections without hostname verification");
}
} else {
socketFactory = null;
}
FailoverServerSet serverSet = new FailoverServerSet(servers.addresses(), servers.ports(), socketFactory, options);
serverSet.setReOrderOnFailover(true);
return serverSet;
}
/**
* This iterates through the configured user templates attempting to open. If all attempts fail, all exceptions
* are combined into one Exception as nested exceptions.
* This iterates through the configured user templates attempting to open. If all attempts fail, the last exception
* is kept as the cause of the thrown exception
*
* @param username a relative name, Not a distinguished name, that will be inserted into the template.
* @return authenticated exception
*/
@Override
public LdapConnection open(String username, SecuredString password) {
//SASL, MD5, etc. all options here stink, we really need to go over ssl + simple authentication
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_CREDENTIALS, password.internalChars());
public LdapSession open(String username, SecuredString password) {
LDAPConnection connection;
try {
connection = ldapServerSet.getConnection();
} catch (LDAPException e) {
throw new ShieldLdapException("failed to connect to any LDAP servers", e);
}
LDAPException lastException = null;
String passwordString = new String(password.internalChars());
for (String template : userDnTemplates) {
String dn = buildDnFromTemplate(username, template);
ldapEnv.put(Context.SECURITY_PRINCIPAL, dn);
try {
DirContext ctx = new InitialDirContext(ldapEnv);
//return the first good connection
return new LdapConnection(connectionLogger, ctx, dn, groupResolver, timeout);
} catch (NamingException e) {
connection.bind(dn, passwordString);
return new LdapSession(connectionLogger, connection, dn, groupResolver, timeout);
} catch (LDAPException e) {
logger.warn("failed LDAP authentication with user template [{}] and DN [{}]", e, template, dn);
lastException = e;
}
}
throw new LdapException("failed LDAP authentication");
connection.close();
throw new ShieldLdapException("failed LDAP authentication", lastException);
}
/**
@ -105,11 +111,11 @@ public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
*/
String buildDnFromTemplate(String username, String template) {
//this value must be escaped to avoid manipulation of the template DN.
String escapedUsername = Rdn.escapeValue(username);
String escapedUsername = escapedRDNValue(username);
return MessageFormat.format(template, escapedUsername);
}
static AbstractLdapConnection.GroupsResolver groupResolver(Settings settings) {
static LdapSession.GroupsResolver groupResolver(Settings settings) {
Settings searchSettings = settings.getAsSettings("group_search");
if (!searchSettings.names().isEmpty()) {
return new SearchGroupsResolver(searchSettings);

View File

@ -5,24 +5,25 @@
*/
package org.elasticsearch.shield.authc.ldap;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.LinkedList;
import java.util.List;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.*;
/**
*
*/
class SearchGroupsResolver implements AbstractLdapConnection.GroupsResolver {
class SearchGroupsResolver implements GroupsResolver {
private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" +
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group))" +
@ -31,7 +32,7 @@ class SearchGroupsResolver implements AbstractLdapConnection.GroupsResolver {
private final String baseDn;
private final String filter;
private final String userAttribute;
private final SearchScope scope;
private final LdapSearchScope scope;
public SearchGroupsResolver(Settings settings) {
baseDn = settings.get("base_dn");
@ -39,43 +40,41 @@ class SearchGroupsResolver implements AbstractLdapConnection.GroupsResolver {
throw new ShieldSettingsException("base_dn must be specified");
}
filter = settings.get("filter", GROUP_SEARCH_DEFAULT_FILTER);
userAttribute = filter == null ? null : settings.get("user_attribute");
scope = SearchScope.resolve(settings.get("scope"), SearchScope.SUB_TREE);
userAttribute = settings.get("user_attribute");
scope = LdapSearchScope.resolve(settings.get("scope"), LdapSearchScope.SUB_TREE);
}
@Override
public List<String> resolve(DirContext ctx, String userDn, TimeValue timeout, ESLogger logger) {
public List<String> resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) {
List<String> groups = new LinkedList<>();
String userId = userAttribute != null ? readUserAttribute(ctx, userDn) : userDn;
SearchControls search = new SearchControls();
search.setReturningAttributes(Strings.EMPTY_ARRAY);
search.setSearchScope(scope.scope());
search.setTimeLimit((int) timeout.millis());
try (ClosableNamingEnumeration<SearchResult> results = new ClosableNamingEnumeration<>(
ctx.search(baseDn, filter, new Object[] { userId }, search))) {
while (results.hasMoreElements()) {
groups.add(results.next().getNameInNamespace());
String userId = userAttribute != null ? readUserAttribute(connection, userDn, timeout, logger) : userDn;
try {
SearchRequest searchRequest = new SearchRequest(baseDn, scope.scope(), createFilter(filter, userId), Strings.EMPTY_ARRAY);
searchRequest.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResult results = search(connection, searchRequest, logger);
for (SearchResultEntry entry : results.getSearchEntries()) {
groups.add(entry.getDN());
}
} catch (NamingException | LdapException e ) {
throw new LdapException("could not search for an LDAP group", userDn, e);
} catch (LDAPException e) {
throw new ShieldLdapException("could not search for LDAP groups", userDn, e);
}
return groups;
}
String readUserAttribute(DirContext ctx, String userDn) {
String readUserAttribute(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) {
try {
Attributes results = ctx.getAttributes(userDn, new String[]{userAttribute});
Attribute attribute = results.get(userAttribute);
if (results.size() == 0) {
throw new LdapException("No results returned for attribute [" + userAttribute + "]", userDn);
SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, userAttribute);
request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResultEntry results = searchForEntry(connection, request, logger);
Attribute attribute = results.getAttribute(userAttribute);
if (attribute == null) {
throw new ShieldLdapException("no results returned for attribute [" + userAttribute + "]", userDn);
}
return (String) attribute.get();
} catch (NamingException e) {
throw new LdapException("Could not look attribute [" + userAttribute + "]", userDn, e);
} catch (ClassCastException e) {
throw new LdapException("Returned ldap attribute [" + userAttribute + "] is not of type String", userDn, e);
return attribute.getValue();
} catch (LDAPException e) {
throw new ShieldLdapException("could not retrieve attribute [" + userAttribute + "]", userDn, e);
}
}
}

View File

@ -8,24 +8,24 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.shield.ShieldException;
/**
* LdapExceptions typically wrap jndi Naming exceptions, and have an additional
* LdapExceptions typically wrap {@link com.unboundid.ldap.sdk.LDAPException}, and have an additional
* parameter of DN attached to each message.
*/
public class LdapException extends ShieldException {
public class ShieldLdapException extends ShieldException {
public LdapException(String msg){
public ShieldLdapException(String msg){
super(msg);
}
public LdapException(String msg, Throwable cause){
public ShieldLdapException(String msg, Throwable cause){
super(msg, cause);
}
public LdapException(String msg, String dn) {
public ShieldLdapException(String msg, String dn) {
this(msg, dn, null);
}
public LdapException(String msg, String dn, Throwable cause) {
public ShieldLdapException(String msg, String dn, Throwable cause) {
super( msg + "; LDAP DN=[" + dn + "]", cause);
}
}

View File

@ -5,24 +5,24 @@
*/
package org.elasticsearch.shield.authc.ldap;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.ldap.support.LdapSession.GroupsResolver;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import java.util.LinkedList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.OBJECT_CLASS_PRESENCE_FILTER;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.searchForEntry;
/**
*
*/
class UserAttributeGroupsResolver implements AbstractLdapConnection.GroupsResolver {
class UserAttributeGroupsResolver implements GroupsResolver {
private final String attribute;
@ -35,25 +35,19 @@ class UserAttributeGroupsResolver implements AbstractLdapConnection.GroupsResolv
}
@Override
public List<String> resolve(DirContext ctx, String userDn, TimeValue timeout, ESLogger logger) {
List<String> groupDns = new LinkedList<>();
public List<String> resolve(LDAPConnection connection, String userDn, TimeValue timeout, ESLogger logger) {
try {
Attributes results = ctx.getAttributes(userDn, new String[] { attribute });
try (ClosableNamingEnumeration<? extends Attribute> ae = new ClosableNamingEnumeration<>(results.getAll())) {
while (ae.hasMore()) {
Attribute attr = ae.next();
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore(); ) {
Object val = attrEnum.next();
if (val instanceof String) {
String stringVal = (String) val;
groupDns.add(stringVal);
}
}
}
SearchRequest request = new SearchRequest(userDn, SearchScope.BASE, OBJECT_CLASS_PRESENCE_FILTER, attribute);
request.setTimeLimitSeconds(Ints.checkedCast(timeout.seconds()));
SearchResultEntry result = searchForEntry(connection, request, logger);
Attribute attributeReturned = result.getAttribute(attribute);
if (attributeReturned == null) {
return Collections.emptyList();
}
} catch (NamingException | LdapException e) {
throw new LdapException("could not look up group attributes for user", userDn, e);
String[] values = attributeReturned.getValues();
return Arrays.asList(values);
} catch (LDAPException e) {
throw new ShieldLdapException("could not look up group attributes for user", userDn, e);
}
return groupDns;
}
}

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.User;
@ -21,13 +21,13 @@ import java.util.Set;
*/
public abstract class AbstractLdapRealm extends CachingUsernamePasswordRealm {
protected final ConnectionFactory connectionFactory;
protected final AbstractGroupToRoleMapper roleMapper;
protected final SessionFactory sessionFactory;
protected final GroupToRoleMapper roleMapper;
protected AbstractLdapRealm(String type, RealmConfig config,
ConnectionFactory connectionFactory, AbstractGroupToRoleMapper roleMapper) {
SessionFactory sessionFactory, GroupToRoleMapper roleMapper) {
super(type, config);
this.connectionFactory = connectionFactory;
this.sessionFactory = sessionFactory;
this.roleMapper = roleMapper;
roleMapper.addListener(new Listener());
}
@ -39,7 +39,7 @@ public abstract class AbstractLdapRealm extends CachingUsernamePasswordRealm {
*/
@Override
protected User doAuthenticate(UsernamePasswordToken token) {
try (AbstractLdapConnection session = connectionFactory.open(token.principal(), token.credentials())) {
try (LdapSession session = sessionFactory.open(token.principal(), token.credentials())) {
List<String> groupDNs = session.groups();
Set<String> roles = roleMapper.mapRoles(groupDNs);
return new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));

View File

@ -3,8 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.LDAPException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.logging.ESLogger;
@ -19,8 +21,6 @@ import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher;
import org.elasticsearch.watcher.ResourceWatcherService;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -30,10 +30,13 @@ import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.dn;
import static org.elasticsearch.shield.authc.ldap.support.LdapUtils.relativeName;
/**
* This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles.
*/
public abstract class AbstractGroupToRoleMapper {
public class GroupToRoleMapper {
public static final String DEFAULT_FILE_NAME = "role_mapping.yml";
public static final String ROLE_MAPPING_FILE_SETTING = "files.role_mapping";
@ -45,11 +48,11 @@ public abstract class AbstractGroupToRoleMapper {
private final String realmType;
private final Path file;
private final boolean useUnmappedGroupsAsRoles;
protected volatile ImmutableMap<LdapName, Set<String>> groupRoles;
protected volatile ImmutableMap<DN, Set<String>> groupRoles;
private CopyOnWriteArrayList<RefreshListener> listeners;
protected AbstractGroupToRoleMapper(String realmType, RealmConfig config, ResourceWatcherService watcherService, @Nullable RefreshListener listener) {
public GroupToRoleMapper(String realmType, RealmConfig config, ResourceWatcherService watcherService, @Nullable RefreshListener listener) {
this.realmType = realmType;
this.config = config;
this.logger = config.logger(getClass());
@ -83,7 +86,7 @@ public abstract class AbstractGroupToRoleMapper {
* logging the error and skipping/removing all mappings. This is aligned with how we handle other auto-loaded files
* in shield.
*/
public static ImmutableMap<LdapName, Set<String>> parseFileLenient(Path path, ESLogger logger, String realmType, String realmName) {
public static ImmutableMap<DN, Set<String>> parseFileLenient(Path path, ESLogger logger, String realmType, String realmName) {
try {
return parseFile(path, logger, realmType, realmName);
} catch (Throwable t) {
@ -92,7 +95,7 @@ public abstract class AbstractGroupToRoleMapper {
}
}
public static ImmutableMap<LdapName, Set<String>> parseFile(Path path, ESLogger logger, String realmType, String realmName) {
public static ImmutableMap<DN, Set<String>> parseFile(Path path, ESLogger logger, String realmType, String realmName) {
logger.trace("reading realm [{}/{}] role mappings file [{}]...", realmType, realmName, path.toAbsolutePath());
@ -105,19 +108,19 @@ public abstract class AbstractGroupToRoleMapper {
.loadFromStream(path.toString(), in)
.build();
Map<LdapName, Set<String>> groupToRoles = new HashMap<>();
Map<DN, Set<String>> groupToRoles = new HashMap<>();
Set<String> roles = settings.names();
for (String role : roles) {
for (String ldapDN : settings.getAsArray(role)) {
try {
LdapName group = new LdapName(ldapDN);
DN group = new DN(ldapDN);
Set<String> groupRoles = groupToRoles.get(group);
if (groupRoles == null) {
groupRoles = new HashSet<>();
groupToRoles.put(group, groupRoles);
}
groupRoles.add(role);
} catch (InvalidNameException e) {
} catch (LDAPException e) {
logger.error("invalid group DN [{}] found in [{}] group to role mappings [{}] for realm [{}/{}]. skipping... ", e, ldapDN, realmType, path.toAbsolutePath(), realmType, realmName);
}
}
@ -145,11 +148,11 @@ public abstract class AbstractGroupToRoleMapper {
public Set<String> mapRoles(List<String> groupDns) {
Set<String> roles = new HashSet<>();
for (String groupDn : groupDns) {
LdapName groupLdapName = LdapUtils.ldapName(groupDn);
DN groupLdapName = dn(groupDn);
if (this.groupRoles.containsKey(groupLdapName)) {
roles.addAll(this.groupRoles.get(groupLdapName));
} else if (useUnmappedGroupsAsRoles) {
roles.add(getRelativeName(groupLdapName));
roles.add(relativeName(groupLdapName));
}
}
if (logger.isDebugEnabled()) {
@ -158,10 +161,6 @@ public abstract class AbstractGroupToRoleMapper {
return roles;
}
String getRelativeName(LdapName groupLdapName) {
return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue();
}
public void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
@ -181,7 +180,7 @@ public abstract class AbstractGroupToRoleMapper {
@Override
public void onFileChanged(File file) {
if (file.equals(AbstractGroupToRoleMapper.this.file.toFile())) {
if (file.equals(GroupToRoleMapper.this.file.toFile())) {
logger.info("role mappings file [{}] changed for realm [{}/{}]. updating mappings...", file.getAbsolutePath(), realmType, config.name());
groupRoles = parseFileLenient(file.toPath(), logger, realmType, config.name());
notifyRefresh();

View File

@ -3,33 +3,33 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import org.elasticsearch.shield.authc.ldap.LdapException;
import com.unboundid.ldap.sdk.SearchScope;
import org.elasticsearch.shield.authc.ldap.ShieldLdapException;
import javax.naming.directory.SearchControls;
import java.util.Locale;
/**
*
*/
public enum SearchScope {
public enum LdapSearchScope {
BASE(SearchControls.OBJECT_SCOPE),
ONE_LEVEL(SearchControls.ONELEVEL_SCOPE),
SUB_TREE(SearchControls.SUBTREE_SCOPE);
BASE(SearchScope.BASE),
ONE_LEVEL(SearchScope.ONE),
SUB_TREE(SearchScope.SUB);
private final int scope;
private final SearchScope scope;
SearchScope(int scope) {
LdapSearchScope(SearchScope scope) {
this.scope = scope;
}
public int scope() {
public SearchScope scope() {
return scope;
}
public static SearchScope resolve(String scope, SearchScope defaultScope) {
public static LdapSearchScope resolve(String scope, LdapSearchScope defaultScope) {
if (scope == null) {
return defaultScope;
}
@ -39,7 +39,7 @@ public enum SearchScope {
case "one_level" : return ONE_LEVEL;
case "sub_tree" : return SUB_TREE;
default:
throw new LdapException("Unknown search scope [" + scope + "]");
throw new ShieldLdapException("Unknown search scope [" + scope + "]");
}
}
}

View File

@ -3,23 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.LDAPConnection;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import java.io.Closeable;
import java.util.List;
/**
* Represents a LDAP connection with an authenticated/bound user that needs closing.
*/
public abstract class AbstractLdapConnection implements Closeable {
public class LdapSession implements Closeable {
protected final ESLogger logger;
protected final DirContext jndiContext;
protected final LDAPConnection ldapConnection;
protected final String bindDn;
protected final GroupsResolver groupsResolver;
protected final TimeValue timeout;
@ -32,25 +31,20 @@ public abstract class AbstractLdapConnection implements Closeable {
* outside of and be reused across all connections. We can't keep a static logger in this class
* since we want the logger to be contextual (i.e. aware of the settings and its environment).
*/
public AbstractLdapConnection(ESLogger logger, DirContext ctx, String boundName, GroupsResolver groupsResolver, TimeValue timeout) {
public LdapSession(ESLogger logger, LDAPConnection connection, String boundName, GroupsResolver groupsResolver, TimeValue timeout) {
this.logger = logger;
this.jndiContext = ctx;
this.ldapConnection = connection;
this.bindDn = boundName;
this.groupsResolver = groupsResolver;
this.timeout = timeout;
}
/**
* LDAP connections should be closed to clean up resources. However, the jndi contexts have the finalize
* implemented properly so that it will clean up on garbage collection.
* LDAP connections should be closed to clean up resources.
*/
@Override
public void close(){
try {
jndiContext.close();
} catch (NamingException e) {
throw new SecurityException("could not close the LDAP connection", e);
}
public void close() {
ldapConnection.close();
}
/**
@ -64,12 +58,12 @@ public abstract class AbstractLdapConnection implements Closeable {
* @return List of fully distinguished group names
*/
public List<String> groups() {
return groupsResolver.resolve(jndiContext, bindDn, timeout, logger);
return groupsResolver.resolve(ldapConnection, bindDn, timeout, logger);
}
public static interface GroupsResolver {
List<String> resolve(DirContext ctx, String userDn, TimeValue timeout, ESLogger logger);
List<String> resolve(LDAPConnection ldapConnection, String userDn, TimeValue timeout, ESLogger logger);
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.shield.authc.ldap.ShieldLdapException;
import javax.naming.ldap.Rdn;
import java.text.MessageFormat;
public final class LdapUtils {
public static final Filter OBJECT_CLASS_PRESENCE_FILTER = Filter.createPresenceFilter("objectClass");
private LdapUtils() {
}
public static DN dn(String dn) {
try {
return new DN(dn);
} catch (LDAPException e) {
throw new ShieldLdapException("invalid DN [" + dn + "]", e);
}
}
public static String relativeName(DN dn) {
return dn.getRDNString().split("=")[1].trim();
}
public static String escapedRDNValue(String rdn) {
// We can't use UnboundID RDN here because it expects attribute=value, not just value
return Rdn.escapeValue(rdn);
}
/**
* This method performs a LDAPConnection.search(...) operation while handling referral exceptions. This is necessary
* to maintain backwards compatibility
* @param ldapConnection
* @param searchRequest
* @param logger
* @return
* @throws LDAPException
*/
public static SearchResult search(LDAPConnection ldapConnection, SearchRequest searchRequest, ESLogger logger) throws LDAPException {
SearchResult results;
try {
results = ldapConnection.search(searchRequest);
} catch (LDAPSearchException e) {
if (e.getResultCode().equals(ResultCode.REFERRAL) && e.getSearchResult() != null) {
if (logger.isDebugEnabled()){
logger.debug("a referral could not be followed for request [{}] so some results may not have been retrieved", e, searchRequest);
}
results = e.getSearchResult();
} else {
throw e;
}
}
return results;
}
/**
* This method performs a LDAPConnection.searchForEntry(...) operation while handling referral exceptions. This is necessary
* to maintain backwards compatibility
* @param ldapConnection
* @param searchRequest
* @param logger
* @return
* @throws LDAPException
*/
public static SearchResultEntry searchForEntry(LDAPConnection ldapConnection, SearchRequest searchRequest, ESLogger logger) throws LDAPException {
SearchResultEntry entry;
try {
entry = ldapConnection.searchForEntry(searchRequest);
} catch (LDAPSearchException e) {
if (e.getResultCode().equals(ResultCode.REFERRAL) && e.getSearchResult() != null && e.getSearchResult().getEntryCount() > 0) {
if (logger.isDebugEnabled()){
logger.debug("a referral could not be followed for request [{}] so some results may not have been retrieved", e, searchRequest);
}
entry = e.getSearchResult().getSearchEntries().get(0);
} else {
throw e;
}
}
return entry;
}
public static Filter createFilter(String filterTemplate, String... arguments) throws LDAPException {
return Filter.create(MessageFormat.format(filterTemplate, (Object[]) encodeFilterValues(arguments)));
}
static String[] encodeFilterValues(String... arguments) {
for(int i = 0; i < arguments.length; i++) {
arguments[i] = Filter.encodeValue(arguments[i]);
}
return arguments;
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPURL;
import com.unboundid.ldap.sdk.ServerSet;
import com.unboundid.util.ssl.HostNameSSLSocketVerifier;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.ssl.ClientSSLService;
import java.util.regex.Pattern;
import static java.util.Arrays.asList;
import static org.elasticsearch.common.base.Predicates.contains;
import static org.elasticsearch.common.collect.Iterables.all;
/**
* This factory holds settings needed for authenticating to LDAP and creating LdapConnections.
* Each created LdapConnection needs to be closed or else connections will pill up consuming resources.
*
* A standard looking usage pattern could look like this:
<pre>
ConnectionFactory factory = ...
try (LdapConnection session = factory.open(...)) {
...do stuff with the session
}
</pre>
*/
public abstract class SessionFactory {
public static final String URLS_SETTING = "url";
public static final String TIMEOUT_TCP_CONNECTION_SETTING = "timeout.tcp_connect";
public static final String TIMEOUT_TCP_READ_SETTING = "timeout.tcp_read";
public static final String TIMEOUT_LDAP_SETTING = "timeout.ldap_search";
public static final String HOSTNAME_VERIFICATION_SETTING = "hostname_verification";
public static final String FOLLOW_REFERRALS_SETTING = "follow_referrals";
public static final TimeValue TIMEOUT_DEFAULT = TimeValue.timeValueSeconds(5);
private static final Pattern STARTS_WITH_LDAPS = Pattern.compile("^ldaps:.*", Pattern.CASE_INSENSITIVE);
private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*", Pattern.CASE_INSENSITIVE);
protected final ESLogger logger;
protected final ESLogger connectionLogger;
protected final RealmConfig config;
protected final TimeValue timeout;
protected SessionFactory(RealmConfig config) {
this.config = config;
this.logger = config.logger(getClass());
this.connectionLogger = config.logger(getClass());
TimeValue searchTimeout = config.settings().getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
if (searchTimeout.millis() < 1000L) {
logger.warn("ldap_search timeout [{}] is less than the minimum supported search timeout of 1s. using 1s", searchTimeout.millis());
searchTimeout = TimeValue.timeValueSeconds(1L);
}
this.timeout = searchTimeout;
}
/**
* Authenticates the given user and opens a new connection that bound to it (meaning, all operations
* under the returned connection will be executed on behalf of the authenticated user.
*
* @param user The name of the user to authenticate the connection with.
* @param password The password of the user
*/
public abstract LdapSession open(String user, SecuredString password);
protected static LDAPConnectionOptions connectionOptions(Settings settings) {
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setConnectTimeoutMillis(Ints.checkedCast(settings.getAsTime(TIMEOUT_TCP_CONNECTION_SETTING, TIMEOUT_DEFAULT).millis()));
options.setFollowReferrals(settings.getAsBoolean(FOLLOW_REFERRALS_SETTING, true));
options.setResponseTimeoutMillis(settings.getAsTime(TIMEOUT_TCP_READ_SETTING, TIMEOUT_DEFAULT).millis());
options.setAutoReconnect(true);
options.setAllowConcurrentSocketFactoryUse(true);
if (settings.getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
options.setSSLSocketVerifier(new HostNameSSLSocketVerifier(true));
}
return options;
}
public static class LDAPServers {
private final String[] addresses;
private final int[] ports;
private final boolean ssl;
public LDAPServers(String[] urls) {
ssl = secureUrls(urls);
addresses = new String[urls.length];
ports = new int[urls.length];
for (int i = 0; i < urls.length; i++) {
try {
LDAPURL url = new LDAPURL(urls[i]);
addresses[i] = url.getHost();
ports[i] = url.getPort();
} catch (LDAPException e) {
throw new ShieldSettingsException("unable to parse configured LDAP url [" + urls[i] +"]", e);
}
}
}
public String[] addresses() {
return addresses;
}
public int[] ports() {
return ports;
}
public boolean ssl() {
return ssl;
}
/**
* @param ldapUrls URLS in the form of "ldap://..." or "ldaps://..."
*/
private boolean secureUrls(String[] ldapUrls) {
if (ldapUrls.length == 0) {
return true;
}
boolean allSecure = all(asList(ldapUrls), contains(STARTS_WITH_LDAPS));
boolean allClear = all(asList(ldapUrls), contains(STARTS_WITH_LDAP));
if (!allSecure && !allClear) {
//No mixing is allowed because we use the same socketfactory
throw new ShieldSettingsException("configured LDAP protocols are not all equal " +
"(ldaps://.. and ldap://..): [" + Strings.arrayToCommaDelimitedString(ldapUrls) + "]");
}
return allSecure;
}
}
}

View File

@ -1,84 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.shield.ssl.ClientSSLService;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.net.InetAddress;
/**
* Abstract class that wraps a SSLSocketFactory and uses it to create sockets for use with LDAP via JNDI
*/
public abstract class AbstractLdapSslSocketFactory extends SocketFactory {
protected static ClientSSLService clientSSLService;
private final SSLSocketFactory socketFactory;
/**
* This should only be invoked once to establish a static instance that will be used for each constructor.
*/
@Inject
public static void init(ClientSSLService sslService) {
AbstractLdapSslSocketFactory.clientSSLService = sslService;
}
public AbstractLdapSslSocketFactory(SSLSocketFactory sslSocketFactory) {
socketFactory = sslSocketFactory;
}
//The following methods are all wrappers around the instance of socketFactory
@Override
public SSLSocket createSocket() throws IOException {
SSLSocket socket = (SSLSocket) socketFactory.createSocket();
configureSSLSocket(socket);
return socket;
}
@Override
public SSLSocket createSocket(String host, int port) throws IOException {
SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port);
configureSSLSocket(socket);
return socket;
}
@Override
public SSLSocket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port, localHost, localPort);
configureSSLSocket(socket);
return socket;
}
@Override
public SSLSocket createSocket(InetAddress host, int port) throws IOException {
SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port);
configureSSLSocket(socket);
return socket;
}
@Override
public SSLSocket createSocket(InetAddress host, int port, InetAddress localHost, int localPort) throws IOException {
SSLSocket socket = (SSLSocket) socketFactory.createSocket(host, port, localHost, localPort);
configureSSLSocket(socket);
return socket;
}
/**
* This method allows for performing additional configuration on each socket. All 'createSocket' methods will
* call this method before returning the socket to the caller. The default implementation is a no-op
* @param sslSocket
*/
protected void configureSSLSocket(SSLSocket sslSocket) {
sslSocket.setEnabledProtocols(clientSSLService.supportedProtocols());
sslSocket.setEnabledCipherSuites(clientSSLService.ciphers());
}
}

View File

@ -1,53 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.shield.authc.ldap.LdapException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import java.io.Closeable;
/**
* ClosableNamingEnumeration wraps a NamingEnumeration so it can be used in a try with resources block and auto-closed.
*/
public class ClosableNamingEnumeration<T> implements Closeable, NamingEnumeration<T> {
private final NamingEnumeration<T> namingEnumeration;
public ClosableNamingEnumeration(NamingEnumeration<T> namingEnumeration) {
this.namingEnumeration = namingEnumeration;
}
@Override
public T next() throws NamingException {
return namingEnumeration.next();
}
@Override
public boolean hasMore() throws NamingException {
return namingEnumeration.hasMore();
}
@Override
public void close() {
try {
namingEnumeration.close();
} catch (NamingException e) {
throw new LdapException("error occurred trying to close a naming enumeration", e);
}
}
@Override
public boolean hasMoreElements() {
return namingEnumeration.hasMoreElements();
}
@Override
public T nextElement() {
return namingEnumeration.nextElement();
}
}

View File

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import java.io.Serializable;
import java.util.regex.Pattern;
import static java.util.Arrays.asList;
import static org.elasticsearch.common.base.Predicates.contains;
import static org.elasticsearch.common.collect.Iterables.all;
/**
* This factory holds settings needed for authenticating to LDAP and creating LdapConnections.
* Each created LdapConnection needs to be closed or else connections will pill up consuming resources.
*
* A standard looking usage pattern could look like this:
<pre>
ConnectionFactory factory = ...
try (LdapConnection session = factory.open(...)) {
...do stuff with the session
}
</pre>
*/
public abstract class ConnectionFactory<Connection extends AbstractLdapConnection> {
public static final String URLS_SETTING = "url";
public static final String JNDI_LDAP_READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout";
public static final String JNDI_LDAP_CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout";
public static final String TIMEOUT_TCP_CONNECTION_SETTING = "timeout.tcp_connect";
public static final String TIMEOUT_TCP_READ_SETTING = "timeout.tcp_read";
public static final String TIMEOUT_LDAP_SETTING = "timeout.ldap_search";
public static final String HOSTNAME_VERIFICATION_SETTING = "hostname_verification";
public static final TimeValue TIMEOUT_DEFAULT = TimeValue.timeValueSeconds(5);
public static final String JAVA_NAMING_LDAP_FACTORY_SOCKET = "java.naming.ldap.factory.socket";
private static final Pattern STARTS_WITH_LDAPS = Pattern.compile("^ldaps:.*", Pattern.CASE_INSENSITIVE);
private static final Pattern STARTS_WITH_LDAP = Pattern.compile("^ldap:.*", Pattern.CASE_INSENSITIVE);
protected final ESLogger logger;
protected final ESLogger connectionLogger;
protected final RealmConfig config;
protected ConnectionFactory(Class<Connection> connectionClass, RealmConfig config) {
this.config = config;
this.logger = config.logger(getClass());
this.connectionLogger = config.logger(connectionClass);
}
/**
* Authenticates the given user and opens a new connection that bound to it (meaning, all operations
* under the returned connection will be executed on behalf of the authenticated user.
*
* @param user The name of the user to authenticate the connection with.
* @param password The password of the user
*/
public abstract Connection open(String user, SecuredString password) ;
/**
* If one of the ldapUrls are SSL this will set the LdapSslSocketFactory as a socket provider on the builder
*
* @param ldapUrls array of ldap urls, either all SSL or none with SSL (no mixing)
* @param builder set of jndi properties, that will
* @throws org.elasticsearch.shield.ShieldSettingsException if URLs have mixed protocols.
*/
protected void configureJndiSSL(String[] ldapUrls, ImmutableMap.Builder<String, Serializable> builder) {
boolean secureProtocol = secureUrls(ldapUrls);
if (secureProtocol) {
if (config.settings().getAsBoolean(HOSTNAME_VERIFICATION_SETTING, true)) {
builder.put(JAVA_NAMING_LDAP_FACTORY_SOCKET, HostnameVerifyingLdapSslSocketFactory.class.getName());
logger.debug("using encryption for LDAP connections with hostname verification");
} else {
builder.put(JAVA_NAMING_LDAP_FACTORY_SOCKET, LdapSslSocketFactory.class.getName());
logger.debug("using encryption for LDAP connections without hostname verification");
}
} else {
logger.warn("encryption not used for LDAP connections");
}
}
/**
* @param ldapUrls URLS in the form of "ldap://..." or "ldaps://..."
* @return true if all URLS are ldaps, also true it ldapUrls is empty. False otherwise
*/
private boolean secureUrls(String[] ldapUrls) {
if (ldapUrls.length == 0) {
return true;
}
boolean allSecure = all(asList(ldapUrls), contains(STARTS_WITH_LDAPS));
boolean allClear = all(asList(ldapUrls), contains(STARTS_WITH_LDAP));
if (!allSecure && !allClear) {
//No mixing is allowed because LdapSSLSocketFactory produces only SSL sockets and not clear text sockets
throw new ShieldSettingsException("configured LDAP protocols are not all equal " +
"(ldaps://.. and ldap://..): [" + Strings.arrayToCommaDelimitedString(ldapUrls) + "]");
}
return allSecure;
}
}

View File

@ -1,67 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import javax.net.SocketFactory;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
* This factory is needed for JNDI configuration for LDAP connections with hostname verification. Each SSLSocket must
* have the appropriate SSLParameters set to indicate that hostname verification is required
*/
public class HostnameVerifyingLdapSslSocketFactory extends AbstractLdapSslSocketFactory {
private static final ESLogger logger = Loggers.getLogger(HostnameVerifyingLdapSslSocketFactory.class);
private static HostnameVerifyingLdapSslSocketFactory instance;
private final SSLParameters sslParameters;
public HostnameVerifyingLdapSslSocketFactory(SSLSocketFactory socketFactory) {
super(socketFactory);
sslParameters = new SSLParameters();
sslParameters.setEndpointIdentificationAlgorithm("LDAPS");
}
/**
* This is invoked by JNDI and the returned SocketFactory must be an HostnameVerifyingLdapSslSocketFactory object
*
* @return a singleton instance of HostnameVerifyingLdapSslSocketFactory set by calling the init static method.
*/
public static synchronized SocketFactory getDefault() {
if (instance == null) {
instance = new HostnameVerifyingLdapSslSocketFactory(clientSSLService.getSSLSocketFactory());
}
return instance;
}
/**
* This clears the static factory. There are threading issues with this. But for
* testing this is useful.
*
* WARNING: THIS METHOD SHOULD ONLY BE CALLED IN TESTS!!!!
*
* TODO: find a way to change the tests such that we can remove this method
*/
public static void clear() {
logger.error("clear should only be called by tests");
instance = null;
}
/**
* Configures the socket to require hostname verification using the LDAPS
* @param sslSocket
*/
@Override
protected void configureSSLSocket(SSLSocket sslSocket) {
super.configureSSLSocket(sslSocket);
sslSocket.setSSLParameters(sslParameters);
}
}

View File

@ -1,56 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.Loggers;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
/**
* This factory is needed for JNDI configuration for LDAP connections. It wraps a single instance of a static
* factory that is initiated by the settings constructor. JNDI uses reflection to call the getDefault() static method
* then checks to make sure that the factory returned is an LdapSslSocketFactory. Because of this we have to wrap
* the socket factory
* <p/>
* http://docs.oracle.com/javase/tutorial/jndi/ldap/ssl.html
*/
public class LdapSslSocketFactory extends AbstractLdapSslSocketFactory {
private static final ESLogger logger = Loggers.getLogger(LdapSslSocketFactory.class);
private static LdapSslSocketFactory instance;
public LdapSslSocketFactory(SSLSocketFactory socketFactory) {
super(socketFactory);
}
/**
* This is invoked by JNDI and the returned SocketFactory must be an LdapSslSocketFactory object
*
* @return a singleton instance of LdapSslSocketFactory set by calling the init static method.
*/
public static synchronized SocketFactory getDefault() {
if (instance == null) {
instance = new LdapSslSocketFactory(clientSSLService.getSSLSocketFactory());
}
return instance;
}
/**
* This clears the static factory. There are threading issues with this. But for
* testing this is useful.
*
* WARNING: THIS METHOD SHOULD ONLY BE CALLED IN TESTS!!!!
*
* TODO: find a way to change the tests such that we can remove this method
*/
public static void clear() {
logger.error("clear should only be called by tests");
instance = null;
}
}

View File

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.shield.authc.ldap.LdapException;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
public final class LdapUtils {
private LdapUtils() {
}
public static LdapName ldapName(String dn) {
try {
return new LdapName(dn);
} catch (InvalidNameException e) {
throw new LdapException("invalid group DN [" + dn + "]", e);
}
}
}

View File

@ -43,7 +43,7 @@ public abstract class AbstractSSLService extends AbstractComponent {
/**
* @return A SSLSocketFactory (for client-side SSL handshaking)
*/
public SSLSocketFactory getSSLSocketFactory() {
public SSLSocketFactory sslSocketFactory() {
return sslContext(ImmutableSettings.EMPTY).getSocketFactory();
}

View File

@ -13,11 +13,11 @@ import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.authz.AuthorizationException;
import org.elasticsearch.shield.transport.netty.ShieldNettyTransport;
import org.elasticsearch.test.ShieldIntegrationTest;
@ -175,7 +175,7 @@ abstract public class AbstractAdLdapRealmTests extends ShieldIntegrationTest {
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", ActiveDirectoryRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".domain_name", "ad.test.elasticsearch.com")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.base_dn", "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? SearchScope.SUB_TREE : SearchScope.ONE_LEVEL)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? LdapSearchScope.SUB_TREE : LdapSearchScope.ONE_LEVEL)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://ad.test.elasticsearch.com:636")
.build()),
@ -184,7 +184,7 @@ abstract public class AbstractAdLdapRealmTests extends ShieldIntegrationTest {
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", LdapRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://ad.test.elasticsearch.com:636")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.base_dn", "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? SearchScope.SUB_TREE : SearchScope.ONE_LEVEL)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? LdapSearchScope.SUB_TREE : LdapSearchScope.ONE_LEVEL)
.putArray(SHIELD_AUTHC_REALMS_EXTERNAL + ".user_dn_templates", "cn={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.build()),
@ -200,7 +200,7 @@ abstract public class AbstractAdLdapRealmTests extends ShieldIntegrationTest {
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".type", LdapRealm.TYPE)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".url", "ldaps://54.200.235.244:636")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.base_dn", "ou=people, dc=oldap, dc=test, dc=elasticsearch, dc=com")
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? SearchScope.SUB_TREE : SearchScope.ONE_LEVEL)
.put(SHIELD_AUTHC_REALMS_EXTERNAL + ".group_search.scope", randomBoolean() ? LdapSearchScope.SUB_TREE : LdapSearchScope.ONE_LEVEL)
.putArray(SHIELD_AUTHC_REALMS_EXTERNAL + ".user_dn_templates", "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com")
.build());

View File

@ -1,31 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapperTests;
import org.elasticsearch.watcher.ResourceWatcherService;
import java.nio.file.Path;
/**
*
*/
public class ActiveDirectoryGroupToRoleMapperTests extends AbstractGroupToRoleMapperTests {
@Override
protected AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService) {
Settings adSettings = ImmutableSettings.builder()
.put("files.role_mapping", file.toAbsolutePath())
.build();
RealmConfig config = new RealmConfig("ad-group-mapper-test", adSettings, settings, env);
return new ActiveDirectoryGroupToRoleMapper(config, watcherService);
}
}

View File

@ -3,15 +3,18 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
package org.elasticsearch.shield.authc.activedirectory;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPURL;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.shield.support.NoOpLogger;
import org.elasticsearch.test.ElasticsearchTestCase;
@ -20,12 +23,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.naming.Context;
import javax.naming.directory.InitialDirContext;
import java.io.Serializable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Hashtable;
import java.util.List;
import java.util.regex.Pattern;
@ -35,50 +34,41 @@ import static org.hamcrest.Matchers.*;
public class ActiveDirectoryGroupsResolverTests extends ElasticsearchTestCase {
public static final String BRUCE_BANNER_DN = "cn=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
private InitialDirContext ldapContext;
private LDAPConnection ldapConnection;
@Before
public void setUp() throws Exception {
super.setUp();
Path keystore = Paths.get(ActiveDirectoryGroupsResolverTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).toAbsolutePath();
Path keystore = Paths.get(ActiveDirectoryGroupsResolverTests.class.getResource("../ldap/support/ldaptrust.jks").toURI()).toAbsolutePath();
/*
* Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext.
* If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname
* verification tests since a re-established connection does not perform hostname verification.
*/
AbstractLdapSslSocketFactory.init(new ClientSSLService(ImmutableSettings.builder()
ClientSSLService clientSSLService = new ClientSSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore.path", keystore)
.put("shield.ssl.keystore.password", "changeit")
.build()));
Hashtable<String, Serializable> ldapEnv = new Hashtable<>();
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, BRUCE_BANNER_DN);
ldapEnv.put(Context.SECURITY_CREDENTIALS, ActiveDirectoryFactoryTests.PASSWORD);
ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
ldapEnv.put(Context.PROVIDER_URL, ActiveDirectoryFactoryTests.AD_LDAP_URL);
ldapEnv.put(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET, LdapSslSocketFactory.class.getName());
ldapEnv.put("java.naming.ldap.attributes.binary", "tokenGroups");
ldapEnv.put(Context.REFERRAL, "follow");
ldapContext = new InitialDirContext(ldapEnv);
.build());
LDAPURL ldapurl = new LDAPURL(ActiveDirectorySessionFactoryTests.AD_LDAP_URL);
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setFollowReferrals(true);
options.setAutoReconnect(true);
options.setAllowConcurrentSocketFactoryUse(true);
options.setConnectTimeoutMillis(Ints.checkedCast(SessionFactory.TIMEOUT_DEFAULT.millis()));
options.setResponseTimeoutMillis(SessionFactory.TIMEOUT_DEFAULT.millis());
ldapConnection = new LDAPConnection(clientSSLService.sslSocketFactory(), options, ldapurl.getHost(), ldapurl.getPort(), BRUCE_BANNER_DN, ActiveDirectorySessionFactoryTests.PASSWORD);
}
@After
public void tearDown() throws Exception {
super.tearDown();
ldapContext.close();
LdapSslSocketFactory.clear();
ldapConnection.close();
}
@Test
public void testResolveSubTree() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("scope", SearchScope.SUB_TREE)
.put("scope", LdapSearchScope.SUB_TREE)
.build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, containsInAnyOrder(
containsString("Avengers"),
containsString("SHIELD"),
@ -92,22 +82,22 @@ public class ActiveDirectoryGroupsResolverTests extends ElasticsearchTestCase {
@Test
public void testResolveOneLevel() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("scope", SearchScope.ONE_LEVEL)
.put("scope", LdapSearchScope.ONE_LEVEL)
.put("base_dn", "CN=Builtin, DC=ad, DC=test, DC=elasticsearch,DC=com")
.build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, hasItem(containsString("Users")));
}
@Test
public void testResolveBaseLevel() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("scope", SearchScope.BASE)
.put("scope", LdapSearchScope.BASE)
.put("base_dn", "CN=Users, CN=Builtin, DC=ad, DC=test, DC=elasticsearch, DC=com")
.build();
ActiveDirectoryGroupsResolver resolver = new ActiveDirectoryGroupsResolver(settings, "DC=ad,DC=test,DC=elasticsearch,DC=com");
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, hasItem(containsString("Users")));
}
@ -119,7 +109,7 @@ public class ActiveDirectoryGroupsResolverTests extends ElasticsearchTestCase {
"S-1-5-32-545", //Default Users group
"S-1-5-21-3510024162-210737641-214529065-513" //Default Domain Users group
};
String query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapContext, "CN=Jarvis, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10));
Filter query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, "CN=Jarvis, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertValidSidQuery(query, expectedSids);
}
@ -129,7 +119,7 @@ public class ActiveDirectoryGroupsResolverTests extends ElasticsearchTestCase {
"S-1-5-32-545", //Default Users group
"S-1-5-21-3510024162-210737641-214529065-513", //Default Domain Users group
"S-1-5-21-3510024162-210737641-214529065-1117"}; //Gods group
String query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapContext, "CN=Odin, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10));
Filter query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, "CN=Odin, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertValidSidQuery(query, expectedSids);
}
@ -143,16 +133,17 @@ public class ActiveDirectoryGroupsResolverTests extends ElasticsearchTestCase {
"S-1-5-21-3510024162-210737641-214529065-1108", //Geniuses
"S-1-5-21-3510024162-210737641-214529065-1106", //SHIELD
"S-1-5-21-3510024162-210737641-214529065-1105"};//Avengers
String query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapContext, "CN=Bruce Banner, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10));
Filter query = ActiveDirectoryGroupsResolver.buildGroupQuery(ldapConnection, "CN=Bruce Banner, CN=Users, DC=ad, DC=test, DC=elasticsearch, DC=com", TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertValidSidQuery(query, expectedSids);
}
}
private void assertValidSidQuery(String query, String[] expectedSids) {
private void assertValidSidQuery(Filter query, String[] expectedSids) {
String queryString = query.toString();
Pattern sidQueryPattern = Pattern.compile("\\(\\|(\\(objectSid=S(-\\d+)+\\))+\\)");
assertThat("[" + query + "] didn't match the search filter pattern", sidQueryPattern.matcher(query).matches(), is(true));
assertThat("[" + queryString + "] didn't match the search filter pattern", sidQueryPattern.matcher(queryString).matches(), is(true));
for(String sid: expectedSids) {
assertThat(query, containsString(sid));
assertThat(queryString, containsString(sid));
}
}

View File

@ -3,22 +3,22 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
package org.elasticsearch.shield.authc.activedirectory;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.ldap.LdapConnection;
import org.elasticsearch.shield.authc.ldap.LdapConnectionFactory;
import org.elasticsearch.shield.authc.ldap.LdapException;
import org.elasticsearch.shield.authc.ldap.LdapSessionFactory;
import org.elasticsearch.shield.authc.ldap.ShieldLdapException;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.authc.ldap.support.LdapSession;
import org.elasticsearch.shield.authc.ldap.support.LdapTest;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.ldap.*;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -29,40 +29,36 @@ import java.util.List;
import static org.hamcrest.Matchers.*;
@Network
public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
public class ActiveDirectorySessionFactoryTests extends ElasticsearchTestCase {
public static final String AD_LDAP_URL = "ldaps://54.213.145.20:636";
public static final String PASSWORD = "NickFuryHeartsES";
public static final String AD_DOMAIN = "ad.test.elasticsearch.com";
private ClientSSLService clientSSLService;
@Before
public void initializeSslSocketFactory() throws Exception {
Path keystore = Paths.get(ActiveDirectoryFactoryTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).toAbsolutePath();
Path keystore = Paths.get(ActiveDirectorySessionFactoryTests.class.getResource("../ldap/support/ldaptrust.jks").toURI()).toAbsolutePath();
/*
* Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext.
* If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname
* verification tests since a re-established connection does not perform hostname verification.
*/
AbstractLdapSslSocketFactory.init(new ClientSSLService(ImmutableSettings.builder()
clientSSLService = new ClientSSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore.path", keystore)
.put("shield.ssl.keystore.password", "changeit")
.build()));
}
@After
public void clearSocketFactories() {
LdapSslSocketFactory.clear();
HostnameVerifyingLdapSslSocketFactory.clear();
.build());
}
@Test @SuppressWarnings("unchecked")
public void testAdAuth() {
RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false));
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String userName = "ironman";
try (AbstractLdapConnection ldap = connectionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
containsString("Geniuses"),
@ -77,31 +73,31 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
}
}
@Test @LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elasticsearch/elasticsearch-shield/issues/499")
@Test
public void testTcpReadTimeout() {
Settings settings = ImmutableSettings.builder()
.put(buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false))
.put(ConnectionFactory.HOSTNAME_VERIFICATION_SETTING, false)
.put(ConnectionFactory.TIMEOUT_TCP_READ_SETTING, "1ms")
.put(SessionFactory.HOSTNAME_VERIFICATION_SETTING, false)
.put(SessionFactory.TIMEOUT_TCP_READ_SETTING, "1ms")
.build();
RealmConfig config = new RealmConfig("ad-test", settings);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
try (AbstractLdapConnection ldap = connectionFactory.open("ironman", SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open("ironman", SecuredStringTests.build(PASSWORD))) {
fail("The TCP connection should timeout before getting groups back");
} catch (ActiveDirectoryException e) {
assertThat(e.getCause().getMessage(), containsString("LDAP response read timed out"));
assertThat(e.getCause().getMessage(), containsString("A client-side timeout was encountered while waiting"));
}
}
@Test
public void testAdAuth_avengers() {
RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, false));
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String[] users = new String[]{"cap", "hawkeye", "hulk", "ironman", "thor", "blackwidow", };
for(String user: users) {
try (AbstractLdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
assertThat("group avenger test for user "+user, ldap.groups(), hasItem(Matchers.containsString("Avengers")));
}
}
@ -109,12 +105,12 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
@Test @SuppressWarnings("unchecked")
public void testAuthenticate() {
Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", SearchScope.ONE_LEVEL, false);
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);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String userName = "hulk";
try (AbstractLdapConnection ldap = connectionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
@ -130,12 +126,12 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
@Test @SuppressWarnings("unchecked")
public void testAuthenticate_baseUserSearch() {
Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Bruce Banner, CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", SearchScope.BASE, false);
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);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String userName = "hulk";
try (AbstractLdapConnection ldap = connectionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
@ -152,15 +148,15 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
@Test @SuppressWarnings("unchecked")
public void testAuthenticate_baseGroupSearch() {
Settings settings = ImmutableSettings.builder()
.put(buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", SearchScope.ONE_LEVEL, false))
.put(ActiveDirectoryConnectionFactory.AD_GROUP_SEARCH_BASEDN_SETTING, "CN=Avengers,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com")
.put(ActiveDirectoryConnectionFactory.AD_GROUP_SEARCH_SCOPE_SETTING, SearchScope.BASE)
.put(buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.ONE_LEVEL, false))
.put(ActiveDirectorySessionFactory.AD_GROUP_SEARCH_BASEDN_SETTING, "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);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String userName = "hulk";
try (AbstractLdapConnection ldap = connectionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, hasItem(containsString("Avengers")));
@ -169,13 +165,13 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
@Test @SuppressWarnings("unchecked")
public void testAuthenticate_UserPrincipalName() {
Settings settings = buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", SearchScope.ONE_LEVEL, false);
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);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
//Login with the UserPrincipalName
String userDN;
try (AbstractLdapConnection ldap = connectionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
userDN = ldap.authenticatedUserDn();
assertThat(groups, containsInAnyOrder(
@ -184,7 +180,7 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
containsString("Domain Users")));
}
//Same user but login with sAMAccountName
try (AbstractLdapConnection ldap = connectionFactory.open("selvig", SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open("selvig", SecuredStringTests.build(PASSWORD))) {
assertThat(ldap.authenticatedUserDn(), is(userDN));
List<String> groups = ldap.groups();
@ -198,14 +194,14 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
@Test @SuppressWarnings("unchecked")
public void testCustomUserFilter() {
Settings settings = ImmutableSettings.builder()
.put(buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", SearchScope.SUB_TREE, false))
.put(ActiveDirectoryConnectionFactory.AD_USER_SEARCH_FILTER_SETTING, "(&(objectclass=user)(userPrincipalName={0}@ad.test.elasticsearch.com))")
.put(buildAdSettings(AD_LDAP_URL, AD_DOMAIN, "CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com", LdapSearchScope.SUB_TREE, false))
.put(ActiveDirectorySessionFactory.AD_USER_SEARCH_FILTER_SETTING, "(&(objectclass=user)(userPrincipalName={0}@ad.test.elasticsearch.com))")
.build();
RealmConfig config = new RealmConfig("ad-test", settings);
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
//Login with the UserPrincipalName
try (AbstractLdapConnection ldap = connectionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open("erik.selvig", SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
containsString("Geniuses"),
@ -219,12 +215,12 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
public void testStandardLdapConnection(){
String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
Settings settings = LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, SearchScope.SUB_TREE);
Settings settings = LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE);
RealmConfig config = new RealmConfig("ad-as-ldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String user = "Bruce Banner";
try (LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
@ -240,10 +236,10 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
Settings settings = LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, false);
RealmConfig config = new RealmConfig("ad-as-ldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String user = "Bruce Banner";
try (LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder(
@ -254,53 +250,55 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
}
}
@Test(expected = ActiveDirectoryException.class)
@Test
public void testAdAuthWithHostnameVerification() {
RealmConfig config = new RealmConfig("ad-test", buildAdSettings(AD_LDAP_URL, AD_DOMAIN, true));
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(config);
ActiveDirectorySessionFactory sessionFactory = new ActiveDirectorySessionFactory(config, clientSSLService);
String userName = "ironman";
try (AbstractLdapConnection ldap = connectionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(userName, SecuredStringTests.build(PASSWORD))) {
fail("Test active directory certificate does not have proper hostname/ip address for hostname verification");
} catch (ActiveDirectoryException e) {
assertThat(e.getMessage(), containsString("failed to connect to any active directory servers"));
}
}
@Test(expected = LdapException.class)
@Test(expected = ShieldLdapException.class)
public void testStandardLdapHostnameVerification(){
String groupSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
String userTemplate = "CN={0},CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
Settings settings = ImmutableSettings.builder()
.put(LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, SearchScope.SUB_TREE))
.put(LdapConnectionFactory.HOSTNAME_VERIFICATION_SETTING, true)
.put(LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE))
.put(LdapSessionFactory.HOSTNAME_VERIFICATION_SETTING, true)
.build();
RealmConfig config = new RealmConfig("ad-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String user = "Bruce Banner";
try (LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
fail("Test active directory certificate does not have proper hostname/ip address for hostname verification");
}
}
public static Settings buildAdSettings(String ldapUrl, String adDomainName, boolean hostnameVerification) {
return ImmutableSettings.builder()
.put(ActiveDirectoryConnectionFactory.URLS_SETTING, ldapUrl)
.put(ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
.put(ActiveDirectoryConnectionFactory.HOSTNAME_VERIFICATION_SETTING, hostnameVerification)
.put(ActiveDirectorySessionFactory.URLS_SETTING, ldapUrl)
.put(ActiveDirectorySessionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
.put(ActiveDirectorySessionFactory.HOSTNAME_VERIFICATION_SETTING, hostnameVerification)
.build();
}
public static Settings buildAdSettings(String ldapUrl, String adDomainName, SearchScope scope, String userSearchDN) {
public static Settings buildAdSettings(String ldapUrl, String adDomainName, LdapSearchScope scope, String userSearchDN) {
return buildAdSettings(ldapUrl, adDomainName, userSearchDN, scope, true);
}
public static Settings buildAdSettings(String ldapUrl, String adDomainName, String userSearchDN, SearchScope scope, boolean hostnameVerification) {
public static Settings buildAdSettings(String ldapUrl, String adDomainName, String userSearchDN, LdapSearchScope scope, boolean hostnameVerification) {
return ImmutableSettings.builder()
.putArray(ActiveDirectoryConnectionFactory.URLS_SETTING, ldapUrl)
.put(ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
.put(ActiveDirectoryConnectionFactory.AD_USER_SEARCH_BASEDN_SETTING, userSearchDN)
.put(ActiveDirectoryConnectionFactory.AD_USER_SEARCH_SCOPE_SETTING, scope)
.put(ActiveDirectoryConnectionFactory.HOSTNAME_VERIFICATION_SETTING, hostnameVerification)
.putArray(ActiveDirectorySessionFactory.URLS_SETTING, ldapUrl)
.put(ActiveDirectorySessionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
.put(ActiveDirectorySessionFactory.AD_USER_SEARCH_BASEDN_SETTING, userSearchDN)
.put(ActiveDirectorySessionFactory.AD_USER_SEARCH_SCOPE_SETTING, scope)
.put(ActiveDirectorySessionFactory.HOSTNAME_VERIFICATION_SETTING, hostnameVerification)
.build();
}
}

View File

@ -1,151 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapTest;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.util.List;
import static org.hamcrest.Matchers.*;
public class LdapConnectionFactoryTests extends LdapTest {
public void testBindWithTimeout() throws Exception {
int randomPort = randomIntBetween(49152, 65525); // ephemeral port
// bind own socket locally to not be dependent on the network
try(ServerSocket serverSocket = new ServerSocket()) {
SocketAddress sa = new InetSocketAddress("localhost", randomPort);
serverSocket.setReuseAddress(true);
serverSocket.bind(sa);
String ldapUrl = "ldap://localhost:" + randomPort ;
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=people,o=sevenSeas",
};
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, SearchScope.SUB_TREE))
.put(ConnectionFactory.TIMEOUT_TCP_CONNECTION_SETTING, "1ms") //1 millisecond
.build();
RealmConfig config = new RealmConfig("ldap_realm", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
long start = System.currentTimeMillis();
try (LdapConnection connection = connectionFactory.open(user, userPass)) {
fail("expected connection timeout error here");
} catch (Throwable t) {
long time = System.currentTimeMillis() - start;
assertThat(time, lessThan(1000l));
assertThat(t, instanceOf(LdapException.class));
}
}
}
@Test
public void testBindWithTemplates() {
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=something,ou=obviously,ou=incorrect,o=sevenSeas",
"wrongname={0},ou=people,o=sevenSeas",
"cn={0},ou=people,o=sevenSeas", //this last one should work
};
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplates, groupSearchBase, SearchScope.SUB_TREE));
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapConnection ldap = connectionFactory.open(user, userPass)) {
String dn = ldap.authenticatedUserDn();
assertThat(dn, containsString(user));
}
}
@Test(expected = LdapException.class)
public void testBindWithBogusTemplates() {
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=something,ou=obviously,ou=incorrect,o=sevenSeas",
"wrongname={0},ou=people,o=sevenSeas",
"asdf={0},ou=people,o=sevenSeas", //none of these should work
};
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplates, groupSearchBase, SearchScope.SUB_TREE));
LdapConnectionFactory ldapFac = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapConnection ldapConnection = ldapFac.open(user, userPass)) {
}
}
@Test
public void testGroupLookup_Subtree() {
String groupSearchBase = "o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE));
LdapConnectionFactory ldapFac = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapConnection ldap = ldapFac.open(user, userPass)) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
@Test
public void testGroupLookup_OneLevel() {
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.ONE_LEVEL));
LdapConnectionFactory ldapFac = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
try (LdapConnection ldap = ldapFac.open(user, SecuredStringTests.build("pass"))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
@Test
public void testGroupLookup_Base() {
String groupSearchBase = "cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.BASE));
LdapConnectionFactory ldapFac = new LdapConnectionFactory(config);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapConnection ldap = ldapFac.open(user, userPass)) {
List<String> groups = ldap.groups();
assertThat(groups.size(), is(1));
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
}

View File

@ -1,75 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapper;
import org.elasticsearch.shield.authc.support.ldap.AbstractGroupToRoleMapperTests;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
import static org.hamcrest.Matchers.hasItems;
public class LdapGroupToRoleMapperTests extends AbstractGroupToRoleMapperTests {
private final String[] starkGroupDns = new String[] {
//groups can be named by different attributes, depending on the directory,
//we don't care what it is named by
"cn=shield,ou=marvel,o=superheros",
"cn=avengers,ou=marvel,o=superheros",
"group=genius, dc=mit, dc=edu",
"groupName = billionaire , ou = acme",
"gid = playboy , dc = example , dc = com",
"groupid=philanthropist,ou=groups,dc=unitedway,dc=org"
};
@Override
protected AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService) {
Settings ldapSettings = ImmutableSettings.builder()
.put("files.role_mapping", file.toAbsolutePath())
.build();
RealmConfig config = new RealmConfig("ldap-group-mapper-test", ldapSettings, settings, env);
return new LdapGroupToRoleMapper(config, watcherService);
}
@Test
public void testYaml() throws IOException {
File file = this.getResource("../support/ldap/role_mapping.yml");
Settings ldapSettings = ImmutableSettings.settingsBuilder()
.put(LdapGroupToRoleMapper.ROLE_MAPPING_FILE_SETTING, file.getCanonicalPath())
.build();
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
AbstractGroupToRoleMapper mapper = new LdapGroupToRoleMapper(config, new ResourceWatcherService(settings, threadPool));
Set<String> roles = mapper.mapRoles( Arrays.asList(starkGroupDns) );
//verify
assertThat(roles, hasItems("shield", "avenger"));
}
@Test
public void testRelativeDN() {
Settings ldapSettings = ImmutableSettings.builder()
.put(AbstractGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
AbstractGroupToRoleMapper mapper = new LdapGroupToRoleMapper(config, new ResourceWatcherService(settings, threadPool));
Set<String> roles = mapper.mapRoles(Arrays.asList(starkGroupDns));
assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers"));
}
}

View File

@ -13,8 +13,9 @@ import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.authc.support.ldap.LdapTest;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.elasticsearch.shield.authc.ldap.support.GroupToRoleMapper;
import org.elasticsearch.shield.authc.ldap.support.LdapTest;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.After;
@ -52,17 +53,17 @@ public class LdapRealmTest extends LdapTest {
@Test
public void testRestHeaderRegistration() {
new LdapRealm.Factory(resourceWatcherService, restController);
new LdapRealm.Factory(resourceWatcherService, restController, null);
verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
}
@Test
public void testAuthenticate_SubTreeGroupSearch(){
public void testAuthenticate_SubTreeGroupSearch() throws Exception {
String groupSearchBase = "o=sevenSeas";
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE);
Settings settings = buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE);
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
LdapConnectionFactory ldapFactory = new LdapConnectionFactory(config);
LdapSessionFactory ldapFactory = new LdapSessionFactory(config, null);
LdapRealm ldap = new LdapRealm(config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService));
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
@ -71,32 +72,32 @@ public class LdapRealmTest extends LdapTest {
}
@Test
public void testAuthenticate_OneLevelGroupSearch(){
public void testAuthenticate_OneLevelGroupSearch() throws Exception {
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.ONE_LEVEL))
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL))
.build();
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
LdapConnectionFactory ldapFactory = new LdapConnectionFactory(config);
LdapSessionFactory ldapFactory = new LdapSessionFactory(config, null);
LdapRealm ldap = new LdapRealm(config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService));
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
assertThat( user, notNullValue());
assertThat(user, notNullValue());
assertThat(user.roles(), arrayContaining("HMS Victory"));
}
@Test
public void testAuthenticate_Caching(){
public void testAuthenticate_Caching() throws Exception {
String groupSearchBase = "o=sevenSeas";
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE))
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE))
.build();
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
LdapConnectionFactory ldapFactory = new LdapConnectionFactory(config);
LdapSessionFactory ldapFactory = new LdapSessionFactory(config, null);
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm(config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService));
ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
@ -107,16 +108,16 @@ public class LdapRealmTest extends LdapTest {
}
@Test
public void testAuthenticate_Caching_Refresh(){
public void testAuthenticate_Caching_Refresh() throws Exception {
String groupSearchBase = "o=sevenSeas";
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE))
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE))
.build();
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
LdapConnectionFactory ldapFactory = new LdapConnectionFactory(config);
LdapGroupToRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService);
LdapSessionFactory ldapFactory = new LdapSessionFactory(config, null);
GroupToRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService);
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm(config, ldapFactory, roleMapper);
ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
@ -134,16 +135,16 @@ public class LdapRealmTest extends LdapTest {
}
@Test
public void testAuthenticate_Noncaching(){
public void testAuthenticate_Noncaching() throws Exception {
String groupSearchBase = "o=sevenSeas";
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, SearchScope.SUB_TREE))
.put(buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE))
.put(LdapRealm.CACHE_TTL_SETTING, -1)
.build();
RealmConfig config = new RealmConfig("test-ldap-realm", settings);
LdapConnectionFactory ldapFactory = new LdapConnectionFactory(config);
LdapSessionFactory ldapFactory = new LdapSessionFactory(config, null);
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm(config, ldapFactory, buildGroupAsRoleMapper(resourceWatcherService));
ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));

View File

@ -0,0 +1,175 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.ldap.support.LdapSession;
import org.elasticsearch.shield.authc.ldap.support.LdapTest;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.Test;
import java.util.List;
import static org.hamcrest.Matchers.*;
public class LdapSessionFactoryTests extends LdapTest {
@Test
public void testBindWithReadTimeout() throws Exception {
String ldapUrl = ldapUrl();
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=people,o=sevenSeas",
};
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, LdapSearchScope.SUB_TREE))
.put(SessionFactory.TIMEOUT_TCP_READ_SETTING, "1ms") //1 millisecond
.build();
RealmConfig config = new RealmConfig("ldap_realm", settings);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
ldapServer.setProcessingDelayMillis(500L);
long start = System.currentTimeMillis();
try (LdapSession session = sessionFactory.open(user, userPass)) {
fail("expected connection timeout error here");
} catch (Throwable t) {
long time = System.currentTimeMillis() - start;
assertThat(time, lessThan(1000l));
assertThat(t, instanceOf(ShieldLdapException.class));
} finally {
ldapServer.setProcessingDelayMillis(0L);
}
}
@Test
@Network
public void testConnectTimeout() {
// Local sockets connect too fast...
String ldapUrl = "ldap://elasticsearch.com:389";
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=people,o=sevenSeas",
};
Settings settings = ImmutableSettings.builder()
.put(buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, LdapSearchScope.SUB_TREE))
.put(SessionFactory.TIMEOUT_TCP_CONNECTION_SETTING, "1ms") //1 millisecond
.build();
RealmConfig config = new RealmConfig("ldap_realm", settings);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
long start = System.currentTimeMillis();
try (LdapSession session = sessionFactory.open(user, userPass)) {
fail("expected connection timeout error here");
} catch (Throwable t) {
long time = System.currentTimeMillis() - start;
assertThat(time, lessThan(10000l));
assertThat(t, instanceOf(ShieldLdapException.class));
assertThat(t.getCause().getCause().getMessage(), containsString("within the configured timeout of"));
}
}
@Test
public void testBindWithTemplates() throws Exception {
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=something,ou=obviously,ou=incorrect,o=sevenSeas",
"wrongname={0},ou=people,o=sevenSeas",
"cn={0},ou=people,o=sevenSeas", //this last one should work
};
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplates, groupSearchBase, LdapSearchScope.SUB_TREE));
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapSession ldap = sessionFactory.open(user, userPass)) {
String dn = ldap.authenticatedUserDn();
assertThat(dn, containsString(user));
}
}
@Test(expected = ShieldLdapException.class)
public void testBindWithBogusTemplates() throws Exception {
String groupSearchBase = "o=sevenSeas";
String[] userTemplates = new String[] {
"cn={0},ou=something,ou=obviously,ou=incorrect,o=sevenSeas",
"wrongname={0},ou=people,o=sevenSeas",
"asdf={0},ou=people,o=sevenSeas", //none of these should work
};
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplates, groupSearchBase, LdapSearchScope.SUB_TREE));
LdapSessionFactory ldapFac = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapSession ldapConnection = ldapFac.open(user, userPass)) {
}
}
@Test
public void testGroupLookup_Subtree() throws Exception {
String groupSearchBase = "o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.SUB_TREE));
LdapSessionFactory ldapFac = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapSession ldap = ldapFac.open(user, userPass)) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
@Test
public void testGroupLookup_OneLevel() throws Exception {
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL));
LdapSessionFactory ldapFac = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
try (LdapSession ldap = ldapFac.open(user, SecuredStringTests.build("pass"))) {
List<String> groups = ldap.groups();
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
@Test
public void testGroupLookup_Base() throws Exception {
String groupSearchBase = "cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
RealmConfig config = new RealmConfig("ldap_realm", buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, LdapSearchScope.BASE));
LdapSessionFactory ldapFac = new LdapSessionFactory(config, null);
String user = "Horatio Hornblower";
SecuredString userPass = SecuredStringTests.build("pass");
try (LdapSession ldap = ldapFac.open(user, userPass)) {
List<String> groups = ldap.groups();
assertThat(groups.size(), is(1));
assertThat(groups, containsInAnyOrder("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
}
}

View File

@ -5,16 +5,17 @@
*/
package org.elasticsearch.shield.authc.ldap;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.authc.ldap.support.LdapSession;
import org.elasticsearch.shield.authc.ldap.support.LdapTest;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.ldap.*;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -30,41 +31,36 @@ public class OpenLdapTests extends ElasticsearchTestCase {
public static final String OPEN_LDAP_URL = "ldaps://54.200.235.244:636";
public static final String PASSWORD = "NickFuryHeartsES";
public ClientSSLService clientSSLService;
@Before
public void initializeSslSocketFactory() throws Exception {
Path keystore = Paths.get(OpenLdapTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).toAbsolutePath();
Path keystore = Paths.get(OpenLdapTests.class.getResource("../ldap/support/ldaptrust.jks").toURI()).toAbsolutePath();
/*
* Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext.
* If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname
* verification tests since a re-established connection does not perform hostname verification.
*/
AbstractLdapSslSocketFactory.init(new ClientSSLService(ImmutableSettings.builder()
clientSSLService = new ClientSSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore.path", keystore)
.put("shield.ssl.keystore.password", "changeit")
.build()));
}
@After
public void clearSocketFactories() {
LdapSslSocketFactory.clear();
HostnameVerifyingLdapSslSocketFactory.clear();
.build());
}
@Test
public void testConnect() {
//openldap does not use cn as naming attributes by default
String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
RealmConfig config = new RealmConfig("oldap-test", LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.ONE_LEVEL));
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
RealmConfig config = new RealmConfig("oldap-test", LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL));
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String[] users = new String[] { "blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor" };
for (String user : users) {
LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD));
assertThat(ldap.groups(), hasItem(containsString("Avengers")));
ldap.close();
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
assertThat(ldap.groups(), hasItem(containsString("Avengers")));
}
}
}
@ -74,12 +70,12 @@ public class OpenLdapTests extends ElasticsearchTestCase {
String groupSearchBase = "cn=Avengers,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
RealmConfig config = new RealmConfig("oldap-test", LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.BASE));
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
RealmConfig config = new RealmConfig("oldap-test", LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.BASE));
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String[] users = new String[] { "blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor" };
for (String user : users) {
LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD));
LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD));
assertThat(ldap.groups(), hasItem(containsString("Avengers")));
ldap.close();
}
@ -90,77 +86,55 @@ public class OpenLdapTests extends ElasticsearchTestCase {
String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
Settings settings = ImmutableSettings.builder()
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.ONE_LEVEL))
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL))
.put("group_search.filter", "(&(objectclass=posixGroup)(memberUID={0}))")
.put("group_search.user_attribute", "uid")
.build();
RealmConfig config = new RealmConfig("oldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
try (LdapConnection ldap = connectionFactory.open("selvig", SecuredStringTests.build(PASSWORD))){
try (LdapSession ldap = sessionFactory.open("selvig", SecuredStringTests.build(PASSWORD))){
assertThat(ldap.groups(), hasItem(containsString("Geniuses")));
}
}
@Test @LuceneTestCase.AwaitsFix(bugUrl = "https://github.com/elasticsearch/elasticsearch-shield/issues/499")
@Test
public void testTcpTimeout() {
String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
Settings settings = ImmutableSettings.builder()
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.ONE_LEVEL))
.put(ConnectionFactory.HOSTNAME_VERIFICATION_SETTING, false)
.put(ConnectionFactory.TIMEOUT_TCP_READ_SETTING, "1ms") //1 millisecond
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL))
.put(SessionFactory.HOSTNAME_VERIFICATION_SETTING, false)
.put(SessionFactory.TIMEOUT_TCP_READ_SETTING, "1ms") //1 millisecond
.build();
RealmConfig config = new RealmConfig("oldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
try (LdapConnection ldap = connectionFactory.open("thor", SecuredStringTests.build(PASSWORD))){
ldap.groups();
try (LdapSession ldap = sessionFactory.open("thor", SecuredStringTests.build(PASSWORD))){
fail("The TCP connection should timeout before getting groups back");
} catch (LdapException e) {
assertThat(e.getCause().getMessage(), containsString("LDAP response read timed out"));
} catch (ShieldLdapException e) {
assertThat(e.getCause().getMessage(), containsString("A client-side timeout was encountered while waiting"));
}
}
@Test
public void testLdapTimeout() {
String groupSearchBase = "dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
Settings settings = ImmutableSettings.builder()
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.SUB_TREE))
.put(ConnectionFactory.HOSTNAME_VERIFICATION_SETTING, false)
.put(ConnectionFactory.TIMEOUT_LDAP_SETTING, "1ms") //1 millisecond
.build();
RealmConfig config = new RealmConfig("oldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
try (LdapConnection ldap = connectionFactory.open("thor", SecuredStringTests.build(PASSWORD))) {
ldap.groups();
fail("The server should timeout the group request");
} catch (LdapException e) {
assertThat(e.getCause().getMessage(), containsString("error code 32")); //openldap response for timeout
}
}
@Test(expected = LdapException.class)
public void testStandardLdapConnectionHostnameVerification() {
//openldap does not use cn as naming attributes by default
String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
Settings settings = ImmutableSettings.builder()
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, SearchScope.ONE_LEVEL))
.put(LdapConnectionFactory.HOSTNAME_VERIFICATION_SETTING, true)
.put(LdapTest.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, LdapSearchScope.ONE_LEVEL))
.put(LdapSessionFactory.HOSTNAME_VERIFICATION_SETTING, true)
.build();
RealmConfig config = new RealmConfig("oldap-test", settings);
LdapConnectionFactory connectionFactory = new LdapConnectionFactory(config);
LdapSessionFactory sessionFactory = new LdapSessionFactory(config, clientSSLService);
String user = "blackwidow";
try (LdapConnection ldap = connectionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
try (LdapSession ldap = sessionFactory.open(user, SecuredStringTests.build(PASSWORD))) {
fail("OpenLDAP certificate does not contain the correct hostname/ip so hostname verification should fail on open");
} catch (ShieldLdapException e) {
assertThat(e.getMessage(), containsString("failed to connect to any LDAP servers"));
}
}
}

View File

@ -5,14 +5,16 @@
*/
package org.elasticsearch.shield.authc.ldap;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPURL;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.authc.ldap.support.LdapSearchScope;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.shield.support.NoOpLogger;
import org.elasticsearch.test.ElasticsearchTestCase;
@ -21,12 +23,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.naming.Context;
import javax.naming.directory.InitialDirContext;
import java.io.Serializable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Hashtable;
import java.util.List;
import static org.hamcrest.Matchers.*;
@ -35,41 +33,32 @@ import static org.hamcrest.Matchers.*;
public class SearchGroupsResolverTests extends ElasticsearchTestCase {
public static final String BRUCE_BANNER_DN = "uid=hulk,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
private InitialDirContext ldapContext;
private LDAPConnection ldapConnection;
@Before
public void setup() throws Exception {
super.setUp();
Path keystore = Paths.get(SearchGroupsResolverTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).toAbsolutePath();
/*
* Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext.
* If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname
* verification tests since a re-established connection does not perform hostname verification.
*/
AbstractLdapSslSocketFactory.init(new ClientSSLService(ImmutableSettings.builder()
Path keystore = Paths.get(SearchGroupsResolverTests.class.getResource("../ldap/support/ldaptrust.jks").toURI()).toAbsolutePath();
ClientSSLService clientSSLService = new ClientSSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore.path", keystore)
.put("shield.ssl.keystore.password", "changeit")
.build()));
Hashtable<String, Serializable> ldapEnv = new Hashtable<>();
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, BRUCE_BANNER_DN);
ldapEnv.put(Context.SECURITY_CREDENTIALS, OpenLdapTests.PASSWORD);
ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
ldapEnv.put(Context.PROVIDER_URL, OpenLdapTests.OPEN_LDAP_URL);
ldapEnv.put(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET, LdapSslSocketFactory.class.getName());
ldapEnv.put("java.naming.ldap.attributes.binary", "tokenGroups");
ldapEnv.put(Context.REFERRAL, "follow");
ldapContext = new InitialDirContext(ldapEnv);
.build());
LDAPURL ldapurl = new LDAPURL(OpenLdapTests.OPEN_LDAP_URL);
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setFollowReferrals(true);
options.setAutoReconnect(true);
options.setAllowConcurrentSocketFactoryUse(true);
options.setConnectTimeoutMillis(Ints.checkedCast(SessionFactory.TIMEOUT_DEFAULT.millis()));
options.setResponseTimeoutMillis(SessionFactory.TIMEOUT_DEFAULT.millis());
ldapConnection = new LDAPConnection(clientSSLService.sslSocketFactory(), options, ldapurl.getHost(), ldapurl.getPort(), BRUCE_BANNER_DN, OpenLdapTests.PASSWORD);
}
@After
public void tearDown() throws Exception {
super.tearDown();
ldapContext.close();
LdapSslSocketFactory.clear();
ldapConnection.close();
}
@Test
@ -79,7 +68,7 @@ public class SearchGroupsResolverTests extends ElasticsearchTestCase {
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, containsInAnyOrder(
containsString("Avengers"),
containsString("SHIELD"),
@ -91,11 +80,11 @@ public class SearchGroupsResolverTests extends ElasticsearchTestCase {
public void testResolveOneLevel() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("scope", SearchScope.ONE_LEVEL)
.put("scope", LdapSearchScope.ONE_LEVEL)
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, containsInAnyOrder(
containsString("Avengers"),
containsString("SHIELD"),
@ -107,11 +96,11 @@ public class SearchGroupsResolverTests extends ElasticsearchTestCase {
public void testResolveBase() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "cn=Avengers,ou=People,dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("scope", SearchScope.BASE)
.put("scope", LdapSearchScope.BASE)
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, hasItem(containsString("Avengers")));
}
@ -124,65 +113,66 @@ public class SearchGroupsResolverTests extends ElasticsearchTestCase {
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, "uid=selvig,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com", TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, "uid=selvig,ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com", TimeValue.timeValueSeconds(10), NoOpLogger.INSTANCE);
assertThat(groups, hasItem(containsString("Geniuses")));
}
@Test
public void testSearchWithoutSpecifyingBaseDN() throws Exception {
public void testCreateWithoutSpecifyingBaseDN() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("scope", SearchScope.SUB_TREE)
.put("scope", LdapSearchScope.SUB_TREE)
.build();
try {
new SearchGroupsResolver(settings);
fail("base_dn must be specified and an exception should have been thrown");
} catch (ShieldSettingsException e) {
assertThat(e.getMessage(), containsString("base_dn must be specified"));
}
}
@Test
public void testReadUserAttribute() throws Exception {
{
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "uid")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
assertThat(resolver.readUserAttribute(ldapContext, BRUCE_BANNER_DN), is("hulk"));
}
public void testReadUserAttributeUid() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "uid").build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
assertThat(resolver.readUserAttribute(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE), is("hulk"));
}
{
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "cn")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
assertThat(resolver.readUserAttribute(ldapContext, BRUCE_BANNER_DN), is("Bruce Banner"));
}
@Test
public void testReadUserAttributeCn() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "cn")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
assertThat(resolver.readUserAttribute(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE), is("Bruce Banner"));
}
@Test
public void testReadNonExistentUserAttribute() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "doesntExists")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
try {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "doesntExists")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
resolver.readUserAttribute(ldapContext, BRUCE_BANNER_DN);
resolver.readUserAttribute(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE);
fail("searching for a non-existing attribute should throw an LdapException");
} catch (LdapException e) {
assertThat(e.getMessage(), containsString("No results returned"));
}
try {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "userPassword")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
resolver.readUserAttribute(ldapContext, BRUCE_BANNER_DN);
fail("searching for a binary attribute should throw an LdapException");
} catch (LdapException e) {
assertThat(e.getMessage(), containsString("is not of type String"));
} catch (ShieldLdapException e) {
assertThat(e.getMessage(), containsString("no results returned"));
}
}
@Test
public void testReadBinaryUserAttribute() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("base_dn", "dc=oldap,dc=test,dc=elasticsearch,dc=com")
.put("user_attribute", "userPassword")
.build();
SearchGroupsResolver resolver = new SearchGroupsResolver(settings);
String attribute = resolver.readUserAttribute(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(5), NoOpLogger.INSTANCE);
assertThat(attribute, is(notNullValue()));
}
}

View File

@ -5,13 +5,15 @@
*/
package org.elasticsearch.shield.authc.ldap;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPURL;
import org.elasticsearch.common.primitives.Ints;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryFactoryTests;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectorySessionFactoryTests;
import org.elasticsearch.shield.authc.ldap.support.SessionFactory;
import org.elasticsearch.shield.ssl.ClientSSLService;
import org.elasticsearch.shield.support.NoOpLogger;
import org.elasticsearch.test.ElasticsearchTestCase;
@ -20,12 +22,8 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.naming.Context;
import javax.naming.directory.InitialDirContext;
import java.io.Serializable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Hashtable;
import java.util.List;
import static org.hamcrest.Matchers.*;
@ -33,48 +31,39 @@ import static org.hamcrest.Matchers.*;
@Network
public class UserAttributeGroupsResolverTests extends ElasticsearchTestCase {
public static final String BRUCE_BANNER_DN = "cn=Bruce Banner,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
private InitialDirContext ldapContext;
private LDAPConnection ldapConnection;
@Before
public void setUp() throws Exception {
super.setUp();
Path keystore = Paths.get(UserAttributeGroupsResolverTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).toAbsolutePath();
Path keystore = Paths.get(UserAttributeGroupsResolverTests.class.getResource("../ldap/support/ldaptrust.jks").toURI()).toAbsolutePath();
/*
* Prior to each test we reinitialize the socket factory with a new SSLService so that we get a new SSLContext.
* If we re-use a SSLContext, previously connected sessions can get re-established which breaks hostname
* verification tests since a re-established connection does not perform hostname verification.
*/
AbstractLdapSslSocketFactory.init(new ClientSSLService(ImmutableSettings.builder()
ClientSSLService clientSSLService = new ClientSSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore.path", keystore)
.put("shield.ssl.keystore.password", "changeit")
.build()));
Hashtable<String, Serializable> ldapEnv = new Hashtable<>();
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, BRUCE_BANNER_DN);
ldapEnv.put(Context.SECURITY_CREDENTIALS, ActiveDirectoryFactoryTests.PASSWORD);
ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
ldapEnv.put(Context.PROVIDER_URL, ActiveDirectoryFactoryTests.AD_LDAP_URL);
ldapEnv.put(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET, LdapSslSocketFactory.class.getName());
ldapEnv.put("java.naming.ldap.attributes.binary", "tokenGroups");
ldapEnv.put(Context.REFERRAL, "follow");
ldapContext = new InitialDirContext(ldapEnv);
.build());
LDAPURL ldapurl = new LDAPURL(ActiveDirectorySessionFactoryTests.AD_LDAP_URL);
LDAPConnectionOptions options = new LDAPConnectionOptions();
options.setFollowReferrals(true);
options.setAutoReconnect(true);
options.setAllowConcurrentSocketFactoryUse(true);
options.setConnectTimeoutMillis(Ints.checkedCast(SessionFactory.TIMEOUT_DEFAULT.millis()));
options.setResponseTimeoutMillis(SessionFactory.TIMEOUT_DEFAULT.millis());
ldapConnection = new LDAPConnection(clientSSLService.sslSocketFactory(), options, ldapurl.getHost(), ldapurl.getPort(), BRUCE_BANNER_DN, ActiveDirectorySessionFactoryTests.PASSWORD);
}
@After
public void tearDown() throws Exception {
super.tearDown();
ldapContext.close();
LdapSslSocketFactory.clear();
ldapConnection.close();
}
@Test
public void testResolve() throws Exception {
//falling back on the 'memberOf' attribute
UserAttributeGroupsResolver resolver = new UserAttributeGroupsResolver(ImmutableSettings.EMPTY);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
assertThat(groups, containsInAnyOrder(
containsString("Avengers"),
containsString("SHIELD"),
@ -88,7 +77,7 @@ public class UserAttributeGroupsResolverTests extends ElasticsearchTestCase {
.put("user_group_attribute", "seeAlso")
.build();
UserAttributeGroupsResolver resolver = new UserAttributeGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
assertThat(groups, hasItem(containsString("Avengers"))); //seeAlso only has Avengers
}
@ -98,7 +87,7 @@ public class UserAttributeGroupsResolverTests extends ElasticsearchTestCase {
.put("user_group_attribute", "doesntExist")
.build();
UserAttributeGroupsResolver resolver = new UserAttributeGroupsResolver(settings);
List<String> groups = resolver.resolve(ldapContext, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
List<String> groups = resolver.resolve(ldapConnection, BRUCE_BANNER_DN, TimeValue.timeValueSeconds(20), NoOpLogger.INSTANCE);
assertThat(groups, empty());
}
}

View File

@ -3,8 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.DN;
import org.elasticsearch.common.base.Charsets;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.collect.ImmutableMap;
@ -12,7 +13,9 @@ import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.audit.logfile.CapturingLogger;
import org.elasticsearch.shield.authc.ldap.LdapGroupToRoleMapper;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.activedirectory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
@ -21,10 +24,11 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.naming.ldap.LdapName;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
@ -35,7 +39,18 @@ import static org.hamcrest.Matchers.*;
/**
*
*/
public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCase {
public class GroupToRoleMapperTests extends ElasticsearchTestCase {
private static final String[] STARK_GROUP_DNS = new String[] {
//groups can be named by different attributes, depending on the directory,
//we don't care what it is named by
"cn=shield,ou=marvel,o=superheros",
"cn=avengers,ou=marvel,o=superheros",
"group=genius, dc=mit, dc=edu",
"groupName = billionaire , ou = acme",
"gid = playboy , dc = example , dc = com",
"groupid=philanthropist,ou=groups,dc=unitedway,dc=org"
};
protected Settings settings;
protected Environment env;
@ -55,8 +70,6 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
terminate(threadPool);
}
protected abstract AbstractGroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService);
@Test
public void testMapper_ConfiguredWithUnreadableFile() throws Exception {
Path file = newTempFile().toPath();
@ -64,20 +77,20 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
GroupToRoleMapper mapper = createMapper(file, watcherService);
assertThat(mapper.mappingsCount(), is(0));
}
@Test
public void testMapper_AutoReload() throws Exception {
Path roleMappingFile = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
Path roleMappingFile = Paths.get(GroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
Path file = Files.createTempFile(null, ".yml");
Files.copy(roleMappingFile, file, StandardCopyOption.REPLACE_EXISTING);
final CountDownLatch latch = new CountDownLatch(1);
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
GroupToRoleMapper mapper = createMapper(file, watcherService);
mapper.addListener(new RefreshListener() {
@Override
public void onRefresh() {
@ -110,14 +123,14 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
@Test
public void testMapper_AutoReload_WithParseFailures() throws Exception {
Path roleMappingFile = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
Path roleMappingFile = Paths.get(GroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
Path file = Files.createTempFile(null, ".yml");
Files.copy(roleMappingFile, file, StandardCopyOption.REPLACE_EXISTING);
final CountDownLatch latch = new CountDownLatch(1);
ResourceWatcherService watcherService = new ResourceWatcherService(settings, threadPool);
AbstractGroupToRoleMapper mapper = createMapper(file, watcherService);
GroupToRoleMapper mapper = createMapper(file, watcherService);
mapper.addListener(new RefreshListener() {
@Override
public void onRefresh() {
@ -144,22 +157,22 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
@Test
public void testParseFile() throws Exception {
Path file = Paths.get(AbstractGroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
Path file = Paths.get(GroupToRoleMapperTests.class.getResource("role_mapping.yml").toURI());
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
ImmutableMap<DN, Set<String>> mappings = GroupToRoleMapper.parseFile(file, logger, "_type", "_name");
assertThat(mappings, notNullValue());
assertThat(mappings.size(), is(2));
LdapName ldapName = new LdapName("cn=avengers,ou=marvel,o=superheros");
assertThat(mappings, hasKey(ldapName));
Set<String> roles = mappings.get(ldapName);
DN dn = new DN("cn=avengers,ou=marvel,o=superheros");
assertThat(mappings, hasKey(dn));
Set<String> roles = mappings.get(dn);
assertThat(roles, notNullValue());
assertThat(roles, hasSize(2));
assertThat(roles, containsInAnyOrder("shield", "avenger"));
ldapName = new LdapName("cn=shield,ou=marvel,o=superheros");
assertThat(mappings, hasKey(ldapName));
roles = mappings.get(ldapName);
dn = new DN("cn=shield,ou=marvel,o=superheros");
assertThat(mappings, hasKey(dn));
roles = mappings.get(dn);
assertThat(roles, notNullValue());
assertThat(roles, hasSize(1));
assertThat(roles, containsInAnyOrder("shield"));
@ -169,7 +182,7 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
public void testParseFile_Empty() throws Exception {
Path file = newTempFile().toPath();
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
ImmutableMap<DN, Set<String>> mappings = GroupToRoleMapper.parseFile(file, logger, "_type", "_name");
assertThat(mappings, notNullValue());
assertThat(mappings.isEmpty(), is(true));
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.WARN);
@ -181,7 +194,7 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
public void testParseFile_WhenFileDoesNotExist() throws Exception {
Path file = new File(randomAsciiOfLength(10)).toPath();
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
ImmutableMap<DN, Set<String>> mappings = GroupToRoleMapper.parseFile(file, logger, "_type", "_name");
assertThat(mappings, notNullValue());
assertThat(mappings.isEmpty(), is(true));
}
@ -193,7 +206,7 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
try {
LdapGroupToRoleMapper.parseFile(file, logger, "_type", "_name");
GroupToRoleMapper.parseFile(file, logger, "_type", "_name");
fail("expected a parse failure");
} catch (Exception e) {
this.logger.info("expected", e);
@ -206,11 +219,48 @@ public abstract class AbstractGroupToRoleMapperTests extends ElasticsearchTestCa
// writing in utf_16 should cause a parsing error as we try to read the file in utf_8
Files.write(file, ImmutableList.of("aldlfkjldjdflkjd"), Charsets.UTF_16);
CapturingLogger logger = new CapturingLogger(CapturingLogger.Level.INFO);
ImmutableMap<LdapName, Set<String>> mappings = LdapGroupToRoleMapper.parseFileLenient(file, logger, "_type", "_name");
ImmutableMap<DN, Set<String>> mappings = GroupToRoleMapper.parseFileLenient(file, logger, "_type", "_name");
assertThat(mappings, notNullValue());
assertThat(mappings.isEmpty(), is(true));
List<CapturingLogger.Msg> msgs = logger.output(CapturingLogger.Level.ERROR);
assertThat(msgs.size(), is(1));
assertThat(msgs.get(0).text, containsString("failed to parse role mappings file"));
}
@Test
public void testYaml() throws IOException {
File file = this.getResource("role_mapping.yml");
Settings ldapSettings = ImmutableSettings.settingsBuilder()
.put(GroupToRoleMapper.ROLE_MAPPING_FILE_SETTING, file.getCanonicalPath())
.build();
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
GroupToRoleMapper mapper = new GroupToRoleMapper(LdapRealm.TYPE, config, new ResourceWatcherService(settings, threadPool), null);
Set<String> roles = mapper.mapRoles( Arrays.asList(STARK_GROUP_DNS) );
//verify
assertThat(roles, hasItems("shield", "avenger"));
}
@Test
public void testRelativeDN() {
Settings ldapSettings = ImmutableSettings.builder()
.put(GroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
RealmConfig config = new RealmConfig("ldap1", ldapSettings);
GroupToRoleMapper mapper = new GroupToRoleMapper(LdapRealm.TYPE, config, new ResourceWatcherService(settings, threadPool), null);
Set<String> roles = mapper.mapRoles(Arrays.asList(STARK_GROUP_DNS));
assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers"));
}
protected GroupToRoleMapper createMapper(Path file, ResourceWatcherService watcherService) {
Settings realmSettings = ImmutableSettings.builder()
.put("files.role_mapping", file.toAbsolutePath())
.build();
RealmConfig config = new RealmConfig("ad-group-mapper-test", realmSettings, settings, env);
return new GroupToRoleMapper(randomBoolean() ? ActiveDirectoryRealm.TYPE : LdapRealm.TYPE, config, watcherService, null);
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap.support;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
public class LDAPServersTests extends ElasticsearchTestCase {
@Test
public void testConfigure_1ldaps() {
String[] urls = new String[] { "ldaps://example.com:636" };
SessionFactory.LDAPServers servers = new SessionFactory.LDAPServers(urls);
assertThat(servers.addresses().length, is(equalTo(1)));
assertThat(servers.addresses()[0], is(equalTo("example.com")));
assertThat(servers.ports().length, is(equalTo(1)));
assertThat(servers.ports()[0], is(equalTo(636)));
assertThat(servers.ssl(), is(equalTo(true)));
}
@Test
public void testConfigure_2ldaps() {
String[] urls = new String[] { "ldaps://primary.example.com:636", "LDAPS://secondary.example.com:10636" };
SessionFactory.LDAPServers servers = new SessionFactory.LDAPServers(urls);
assertThat(servers.addresses().length, is(equalTo(2)));
assertThat(servers.addresses()[0], is(equalTo("primary.example.com")));
assertThat(servers.addresses()[1], is(equalTo("secondary.example.com")));
assertThat(servers.ports().length, is(equalTo(2)));
assertThat(servers.ports()[0], is(equalTo(636)));
assertThat(servers.ports()[1], is(equalTo(10636)));
assertThat(servers.ssl(), is(equalTo(true)));
}
@Test
public void testConfigure_2ldap() {
String[] urls = new String[] { "ldap://primary.example.com:392", "LDAP://secondary.example.com:10392" };
SessionFactory.LDAPServers servers = new SessionFactory.LDAPServers(urls);
assertThat(servers.addresses().length, is(equalTo(2)));
assertThat(servers.addresses()[0], is(equalTo("primary.example.com")));
assertThat(servers.addresses()[1], is(equalTo("secondary.example.com")));
assertThat(servers.ports().length, is(equalTo(2)));
assertThat(servers.ports()[0], is(equalTo(392)));
assertThat(servers.ports()[1], is(equalTo(10392)));
assertThat(servers.ssl(), is(equalTo(false)));
}
@Test(expected = ShieldSettingsException.class)
public void testConfigure_1ldaps_1ldap() {
String[] urls = new String[] { "LDAPS://primary.example.com:636", "ldap://secondary.example.com:392" };
new SessionFactory.LDAPServers(urls);
}
@Test(expected = ShieldSettingsException.class)
public void testConfigure_1ldap_1ldaps() {
String[] urls = new String[] { "ldap://primary.example.com:392", "ldaps://secondary.example.com:636" };
new SessionFactory.LDAPServers(urls);
}
}

View File

@ -3,50 +3,53 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
package org.elasticsearch.shield.authc.ldap.support;
import com.carrotsearch.randomizedtesting.LifecycleScope;
import com.carrotsearch.randomizedtesting.ThreadFilter;
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.sdk.*;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.ldap.LdapGroupToRoleMapper;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import static org.elasticsearch.shield.authc.ldap.LdapConnectionFactory.*;
import java.nio.file.Paths;
import static org.elasticsearch.shield.authc.ldap.LdapSessionFactory.*;
@Ignore
@ThreadLeakFilters(defaultFilters = true, filters = { LdapTest.ApachedsThreadLeakFilter.class })
public abstract class LdapTest extends ElasticsearchTestCase {
private static ApacheDsEmbedded ldap;
protected static InMemoryDirectoryServer ldapServer;
@BeforeClass
public static void startLdap() throws Exception {
ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", newTempDir(LifecycleScope.SUITE));
ldap.startServer();
ldapServer = new InMemoryDirectoryServer("o=sevenSeas");
ldapServer.add("o=sevenSeas", new Attribute("dc", "UnboundID"), new Attribute("objectClass", "top", "domain", "extensibleObject"));
ldapServer.importFromLDIF(false, Paths.get(LdapTest.class.getResource("seven-seas.ldif").toURI()).toAbsolutePath().toString());
ldapServer.startListening();
}
@AfterClass
public static void stopLdap() throws Exception {
ldap.stopAndCleanup();
ldap = null;
ldapServer.shutDown(true);
ldapServer = null;
}
protected String ldapUrl() {
return ldap.getUrl();
protected String ldapUrl() throws LDAPException {
LDAPURL url = new LDAPURL("ldap", "localhost", ldapServer.getListenPort(), null, null, null, null);
return url.toString();
}
public static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, SearchScope scope) {
public static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, LdapSearchScope scope) {
return buildLdapSettings(ldapUrl, new String[] { userTemplate }, groupSearchBase, scope);
}
public static Settings buildLdapSettings(String ldapUrl, String[] userTemplate, String groupSearchBase, SearchScope scope) {
public static Settings buildLdapSettings(String ldapUrl, String[] userTemplate, String groupSearchBase, LdapSearchScope scope) {
return ImmutableSettings.builder()
.putArray(URLS_SETTING, ldapUrl)
.putArray(USER_DN_TEMPLATES_SETTING, userTemplate)
@ -62,29 +65,12 @@ public abstract class LdapTest extends ElasticsearchTestCase {
.put(HOSTNAME_VERIFICATION_SETTING, hostnameVerification).build();
}
protected LdapGroupToRoleMapper buildGroupAsRoleMapper(ResourceWatcherService resourceWatcherService) {
protected GroupToRoleMapper buildGroupAsRoleMapper(ResourceWatcherService resourceWatcherService) {
Settings settings = ImmutableSettings.builder()
.put(AbstractGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.put(GroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
RealmConfig config = new RealmConfig("ldap1", settings);
return new LdapGroupToRoleMapper(config, resourceWatcherService);
}
/**
* thread filter because apache ds leaks a thread when LdapServer is started
*/
public final static class ApachedsThreadLeakFilter implements ThreadFilter {
@Override
public boolean reject(Thread t) {
for (StackTraceElement stackTraceElement : t.getStackTrace()) {
if (stackTraceElement.getClassName().startsWith("org.apache.mina.filter.executor.UnorderedThreadPoolExecutor")) {
return true;
}
}
return false;
}
return new GroupToRoleMapper(LdapRealm.TYPE, config, resourceWatcherService, null);
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap.support;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.util.ssl.HostNameSSLSocketVerifier;
import com.unboundid.util.ssl.TrustAllSSLSocketVerifier;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import static org.hamcrest.Matchers.*;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
public class SessionFactoryTests extends ElasticsearchTestCase {
@Test
public void connectionFactoryReturnsCorrectLDAPConnectionOptionsWithDefaultSettings() {
SessionFactory factory = createSessionFactory();
LDAPConnectionOptions options = factory.connectionOptions(ImmutableSettings.EMPTY);
assertThat(options.followReferrals(), is(equalTo(true)));
assertThat(options.allowConcurrentSocketFactoryUse(), is(equalTo(true)));
assertThat(options.getConnectTimeoutMillis(), is(equalTo(5000)));
assertThat(options.getResponseTimeoutMillis(), is(equalTo(5000L)));
assertThat(options.getSSLSocketVerifier(), is(instanceOf(HostNameSSLSocketVerifier.class)));
}
@Test
public void connectionFactoryReturnsCorrectLDAPConnectionOptions() {
Settings settings = settingsBuilder()
.put(SessionFactory.TIMEOUT_TCP_CONNECTION_SETTING, "10ms")
.put(SessionFactory.HOSTNAME_VERIFICATION_SETTING, "false")
.put(SessionFactory.TIMEOUT_TCP_READ_SETTING, "20ms")
.put(SessionFactory.FOLLOW_REFERRALS_SETTING, "false")
.build();
SessionFactory factory = createSessionFactory();
LDAPConnectionOptions options = factory.connectionOptions(settings);
assertThat(options.followReferrals(), is(equalTo(false)));
assertThat(options.allowConcurrentSocketFactoryUse(), is(equalTo(true)));
assertThat(options.getConnectTimeoutMillis(), is(equalTo(10)));
assertThat(options.getResponseTimeoutMillis(), is(equalTo(20L)));
assertThat(options.getSSLSocketVerifier(), is(instanceOf(TrustAllSSLSocketVerifier.class)));
}
private SessionFactory createSessionFactory() {
return new SessionFactory(new RealmConfig("_name")) {
@Override
public LdapSession open(String user, SecuredString password) {
return null;
}
};
}
}

View File

@ -1,267 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import com.carrotsearch.randomizedtesting.SysGlobals;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader;
import org.apache.directory.api.ldap.schemaextractor.SchemaLdifExtractor;
import org.apache.directory.api.ldap.schemaextractor.impl.DefaultSchemaLdifExtractor;
import org.apache.directory.api.ldap.schemaloader.LdifSchemaLoader;
import org.apache.directory.api.ldap.schemamanager.impl.DefaultSchemaManager;
import org.apache.directory.api.util.exception.Exceptions;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.DefaultDirectoryService;
import org.apache.directory.server.core.api.CacheService;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.InstanceLayout;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.schema.SchemaPartition;
import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmIndex;
import org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
import org.apache.directory.server.core.partition.ldif.LdifPartition;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Helper Class to start up an Apache DS LDAP server for testing.
*
* Use ApacheDsRule instead of this class in tests
*/
public class ApacheDsEmbedded {
/**
* The child JVM ordinal of this JVM. Placed by the testing framework. Default is <tt>0</tt> Sequential number starting with 0
*/
public static final int CHILD_JVM_ID = Integer.parseInt(System.getProperty(SysGlobals.CHILDVM_SYSPROP_JVM_ID, "0"));
private final File workDir;
private final String baseDN;
private final String ldifFileName;
private final int port;
/**
* The directory service
*/
private DirectoryService service;
/**
* The LDAP server
*/
private LdapServer server;
/**
* Creates a new instance of EmbeddedADS. It initializes the directory service.
*
* @throws Exception If something went wrong
*/
public ApacheDsEmbedded(String baseDN, String ldifFileName, File workDir) {
this.workDir = workDir;
this.baseDN = baseDN;
this.ldifFileName = ldifFileName;
this.port = 10389 + CHILD_JVM_ID;
}
public int getPort() {
return port;
}
public String getUrl() {
return "ldap://localhost:" + port;
}
/**
* starts the LdapServer
*
* @throws Exception
*/
public void startServer() throws Exception {
initDirectoryService(workDir, baseDN, ldifFileName);
loadSchema(ldifFileName);
server = new LdapServer();
server.setTransports(new TcpTransport(port));
server.setDirectoryService(service);
server.start();
}
/**
* This will cleanup the junk left on the file system and shutdown the server
*
* @throws Exception
*/
public void stopAndCleanup() throws Exception {
if (server != null) server.stop();
if (service != null) service.shutdown();
}
/**
* Add a new partition to the server
*
* @param partitionId The partition Id
* @param partitionDn The partition DN
* @param dnFactory the DN factory
* @return The newly added partition
* @throws Exception If the partition can't be added
*/
private Partition addPartition(String partitionId, String partitionDn, DnFactory dnFactory) throws Exception {
// Create a new partition with the given partition id
JdbmPartition partition = new JdbmPartition(service.getSchemaManager(), dnFactory);
partition.setId(partitionId);
partition.setPartitionPath(new File(service.getInstanceLayout().getPartitionsDirectory(), partitionId).toURI());
partition.setSuffixDn(new Dn(partitionDn));
service.addPartition(partition);
return partition;
}
/**
* Add a new set of index on the given attributes
*
* @param partition The partition on which we want to add index
* @param attrs The list of attributes to index
*/
private void addIndex(Partition partition, String... attrs) {
// Index some attributes on the apache partition
Set indexedAttributes = new HashSet();
for (String attribute : attrs) {
indexedAttributes.add(new JdbmIndex(attribute, false));
}
((JdbmPartition) partition).setIndexedAttributes(indexedAttributes);
}
/**
* initialize the schema manager and add the schema partition to diectory service
*
* @throws Exception if the schema LDIF files are not found on the classpath
*/
private void initSchemaPartition() throws Exception {
InstanceLayout instanceLayout = service.getInstanceLayout();
File schemaPartitionDirectory = new File(instanceLayout.getPartitionsDirectory(), "schema");
// Extract the schema on disk (a brand new one) and load the registries
if (schemaPartitionDirectory.exists()) {
System.out.println("schema partition already exists, skipping schema extraction");
} else {
SchemaLdifExtractor extractor = new DefaultSchemaLdifExtractor(instanceLayout.getPartitionsDirectory());
extractor.extractOrCopy();
}
SchemaLoader loader = new LdifSchemaLoader(schemaPartitionDirectory);
SchemaManager schemaManager = new DefaultSchemaManager(loader);
// We have to load the schema now, otherwise we won't be able
// to initialize the Partitions, as we won't be able to parse
// and normalize their suffix Dn
schemaManager.loadAllEnabled();
List<Throwable> errors = schemaManager.getErrors();
if (errors.size() != 0) {
throw new Exception(I18n.err(I18n.ERR_317, Exceptions.printErrors(errors)));
}
service.setSchemaManager(schemaManager);
// Init the LdifPartition with schema
LdifPartition schemaLdifPartition = new LdifPartition(schemaManager, service.getDnFactory());
schemaLdifPartition.setPartitionPath(schemaPartitionDirectory.toURI());
// The schema partition
SchemaPartition schemaPartition = new SchemaPartition(schemaManager);
schemaPartition.setWrappedPartition(schemaLdifPartition);
service.setSchemaPartition(schemaPartition);
}
/**
* Initialize the server. It creates the partition, adds the index, and
* injects the context entries for the created partitions.
*
* @param workDir the directory to be used for storing the data
* @param baseDn
* @param ldifFileName
* @throws Exception if there were some problems while initializing the system
*/
private void initDirectoryService(File workDir, String baseDn, String ldifFileName) throws Exception {
// Initialize the LDAP service
service = new DefaultDirectoryService();
service.setInstanceLayout(new InstanceLayout(workDir));
CacheService cacheService = new CacheService();
cacheService.initialize(service.getInstanceLayout());
service.setCacheService(cacheService);
// first load the schema
initSchemaPartition();
// then the system partition
// this is a MANDATORY partition
// DO NOT add this via addPartition() method, trunk code complains about duplicate partition
// while initializing
JdbmPartition systemPartition = new JdbmPartition(service.getSchemaManager(), service.getDnFactory());
systemPartition.setId("system");
systemPartition.setPartitionPath(new File(service.getInstanceLayout().getPartitionsDirectory(), systemPartition.getId()).toURI());
systemPartition.setSuffixDn(new Dn(ServerDNConstants.SYSTEM_DN));
systemPartition.setSchemaManager(service.getSchemaManager());
// mandatory to call this method to set the system partition
// Note: this system partition might be removed from trunk
service.setSystemPartition(systemPartition);
// Disable the ChangeLog system
service.getChangeLog().setEnabled(false);
service.setDenormalizeOpAttrsEnabled(true);
Partition apachePartition = addPartition("ldapTest", baseDn, service.getDnFactory());
// Index some attributes on the apache partition
addIndex(apachePartition, "objectClass", "ou", "uid");
// And start the service
service.startup();
// Inject the context entry for dc=Apache,dc=Org partition
if (!service.getAdminSession().exists(apachePartition.getSuffixDn())) {
Dn dnApache = new Dn(baseDn);
Entry entryApache = service.newEntry(dnApache);
entryApache.add("objectClass", "top", "domain", "extensibleObject");
entryApache.add("dc", "Apache");
service.getAdminSession().add(entryApache);
}
}
private void loadSchema(String ldifFileName) throws URISyntaxException {
// Load the directory as a resource
URL dir_url = this.getClass().getResource(ldifFileName);
if (dir_url == null) throw new NullPointerException("the LDIF file doesn't exist: " + ldifFileName);
File ldifFile = new File(dir_url.toURI());
LdifFileLoader ldifLoader = new LdifFileLoader(service.getAdminSession(), ldifFile, Collections.EMPTY_LIST);
ldifLoader.execute();
}
}

View File

@ -1,119 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.hamcrest.Matchers;
import org.junit.Test;
import java.io.Serializable;
import static org.hamcrest.Matchers.equalTo;
public class ConnectionFactoryTests extends ElasticsearchTestCase {
@Test
public void testConfigure_1ldaps() {
String[] urls = new String[] { "ldaps://example.com:636" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.builder();
createConnectionFactoryWithoutHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat((String) settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET),
equalTo(LdapSslSocketFactory.class.getName()));
}
@Test
public void testConfigure_2ldaps() {
String[] urls = new String[] { "ldaps://primary.example.com:636", "LDAPS://secondary.example.com:10636" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithoutHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), Matchers.<Serializable>equalTo(LdapSslSocketFactory.class.getName()));
}
@Test
public void testConfigure_2ldap() {
String[] urls = new String[] { "ldap://primary.example.com:392", "LDAP://secondary.example.com:10392" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithoutHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), equalTo(null));
}
@Test
public void testConfigure_1ldapsWithHostnameVerification() {
String[] urls = new String[] { "ldaps://example.com:636" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.builder();
createConnectionFactoryWithHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat((String) settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET),
equalTo(HostnameVerifyingLdapSslSocketFactory.class.getName()));
}
@Test
public void testConfigure_2ldapsWithHostnameVerification() {
String[] urls = new String[] { "ldaps://primary.example.com:636", "LDAPS://secondary.example.com:10636" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), Matchers.<Serializable>equalTo(HostnameVerifyingLdapSslSocketFactory.class.getName()));
}
@Test
public void testConfigure_2ldapWithHostnameVerification() {
String[] urls = new String[] { "ldap://primary.example.com:392", "LDAP://secondary.example.com:10392" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithHostnameVerification().configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(ConnectionFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), equalTo(null));
}
@Test(expected = ShieldSettingsException.class)
public void testConfigure_1ldaps_1ldap() {
String[] urls = new String[] { "LDAPS://primary.example.com:636", "ldap://secondary.example.com:392" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithoutHostnameVerification().configureJndiSSL(urls, builder);
}
@Test(expected = ShieldSettingsException.class)
public void testConfigure_1ldap_1ldaps() {
String[] urls = new String[] { "ldap://primary.example.com:392", "ldaps://secondary.example.com:636" };
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
createConnectionFactoryWithoutHostnameVerification().configureJndiSSL(urls, builder);
}
private ConnectionFactory createConnectionFactoryWithoutHostnameVerification() {
RealmConfig config = new RealmConfig("_name", ImmutableSettings.builder().put("hostname_verification", false).build());
return new ConnectionFactory<AbstractLdapConnection>(AbstractLdapConnection.class, config) {
@Override
public AbstractLdapConnection open(String user, SecuredString password) {
return null;
}
};
}
private ConnectionFactory createConnectionFactoryWithHostnameVerification() {
return new ConnectionFactory<AbstractLdapConnection>(AbstractLdapConnection.class, new RealmConfig("_name")) {
@Override
public AbstractLdapConnection open(String user, SecuredString password) {
return null;
}
};
}
}

View File

@ -6,13 +6,13 @@
package org.elasticsearch.shield.support;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.apache.commons.lang.ArrayUtils;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Test;
import java.util.Arrays;
import static org.apache.commons.lang.ArrayUtils.addAll;
import static org.apache.commons.lang3.ArrayUtils.add;
import static org.apache.commons.lang3.ArrayUtils.addAll;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
@ -29,7 +29,7 @@ public class ValidationTests extends ElasticsearchTestCase {
private static final char[] numbers = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private static final char[] allowedFirstChars = ArrayUtils.add(alphabet, '_');
private static final char[] allowedFirstChars = add(alphabet, '_');
private static final char[] allowedSubsequent = addAll(addAll(alphabet, numbers), new char[] { '_', '@', '-', '$' });

View File

@ -43,7 +43,7 @@ givenname: Horatio
sn: Hornblower
uid: hhornblo
mail: hhornblo@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=William Bush,ou=people,o=sevenSeas
objectclass: person
@ -57,7 +57,7 @@ manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Bush
uid: wbush
mail: wbush@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=Thomas Quist,ou=people,o=sevenSeas
objectclass: person
@ -71,7 +71,7 @@ manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Quist
uid: tquist
mail: tquist@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=Moultrie Crystal,ou=people,o=sevenSeas
objectclass: person
@ -85,7 +85,7 @@ manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Crystal
uid: mchrysta
mail: mchrysta@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames
@ -110,7 +110,7 @@ givenname: Horatio
sn: Nelson
uid: hnelson
mail: hnelson@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=Thomas Masterman Hardy,ou=people,o=sevenSeas
objectclass: person
@ -124,7 +124,7 @@ manager: cn=Horatio Nelson,ou=people,o=sevenSeas
sn: Hardy
uid: thardy
mail: thardy@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=Cornelius Buckley,ou=people,o=sevenSeas
objectclass: person
@ -138,7 +138,7 @@ manager: cn=Horatio Nelson,ou=people,o=sevenSeas
sn: Buckley
uid: cbuckley
mail: cbuckley@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=HMS Victory,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames
@ -162,7 +162,7 @@ givenname: William
sn: Bligh
uid: wbligh
mail: wbligh@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=Fletcher Christian,ou=people,o=sevenSeas
objectclass: person
@ -176,7 +176,7 @@ manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Christian
uid: fchristi
mail: fchristi@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=John Fryer,ou=people,o=sevenSeas
objectclass: person
@ -190,7 +190,7 @@ manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Fryer
uid: jfryer
mail: jfryer@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=John Hallett,ou=people,o=sevenSeas
objectclass: person
@ -204,7 +204,7 @@ manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Hallett
uid: jhallett
mail: jhallett@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
userpassword: pass
dn: cn=HMS Bounty,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames