mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-09 14:34:43 +00:00
[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:
parent
17e16e2c53
commit
4de8d04f9f
@ -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
13
pom.xml
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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=");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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=");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()]));
|
@ -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();
|
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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)));
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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[] { '_', '@', '-', '$' });
|
||||
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user