LDAP: Implements an LDAP realm

The LDAP realm is a bind-per-user strategy and the group lookup strategy is configurable.
If a role mapping file is not defined, groups names are taken as role names.
Special configuration for active directory simplifies the configuration.

Integration Tests are using an embedded apache DS LDAP Server.

Original commit: elastic/x-pack-elasticsearch@ce20e1b3be
This commit is contained in:
c-a-m 2014-07-22 10:06:54 -06:00
parent cf40c76e15
commit 9cd397727f
22 changed files with 1653 additions and 12 deletions

18
pom.xml
View File

@ -89,6 +89,24 @@
<version>17.0</version> <version>17.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-all</artifactId>
<version>2.0.0-M17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>6.0</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>

View File

@ -33,7 +33,7 @@ public class AuthenticationModule extends AbstractModule implements SpawnModules
modules.add(new ESUsersModule()); modules.add(new ESUsersModule());
} }
if (LdapModule.enabled(settings)) { if (LdapModule.enabled(settings)) {
modules.add(new LdapModule()); modules.add(new LdapModule(settings));
} }
return modules.build(); return modules.build();
} }

View File

@ -0,0 +1,107 @@
/*
* 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.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
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 LDAP 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 AbstractComponent implements LdapConnectionFactory {
public static final String AD_DOMAIN_NAME_SETTING = "domain_name";
public static final String AD_PORT = "default_port";
public static final String AD_USER_SEARCH_BASEDN_SETTING = "user_search_dn";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String userSearchDN;
private final String domainName;
@Inject
public ActiveDirectoryConnectionFactory(Settings settings){
super(settings);
domainName = componentSettings.get(AD_DOMAIN_NAME_SETTING);
if (domainName == null) {
throw new org.elasticsearch.shield.SecurityException("Missing [" + AD_DOMAIN_NAME_SETTING + "] setting for active directory");
}
userSearchDN = componentSettings.get(AD_USER_SEARCH_BASEDN_SETTING, buildDnFromDomain(domainName));
int port = componentSettings.getAsInt(AD_PORT, 389);
String[] ldapUrls = componentSettings.getAsArray(URLS_SETTING, new String[] { "ldap://" + domainName + ":" + port });
sharedLdapEnv = ImmutableMap.<String, Serializable>builder()
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
.put(Context.REFERRAL, "follow")
.build();
}
/**
* 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 LdapConnection bind(String userName, char[] password) {
String userPrincipal = userName + "@" + this.domainName;
Hashtable<String, java.io.Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, userPrincipal);
ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
try {
DirContext ctx = new InitialDirContext(ldapEnv);
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes( new String[0] );
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
NamingEnumeration<SearchResult> results = ctx.search(userSearchDN, searchFilter, new Object[]{ userPrincipal }, searchCtls);
if (results.hasMore()){
SearchResult entry = results.next();
String name = entry.getNameInNamespace();
if (!results.hasMore()) {
//searchByAttribute=true, group subtree search=false, groupSubtreeDN=null
return new LdapConnection(ctx, name, true, false, null);
}
throw new LdapException("Search for user [" + userName + "] by principle name yielded multiple results");
}
throw new LdapException("Search for user [" + userName + "] yielded no results");
} catch (NamingException e) {
throw new LdapException("Unable to authenticate user [" + userName + "] to active directory domain ["+ domainName +"]", e);
}
}
/**
* @param domain active directory domain name
* @return LDAP DN, distinguished name, of the root of the domain
*/
String buildDnFromDomain(String domain) {
return "DC=" + domain.replace(".", ",DC=");
}
}

View File

@ -0,0 +1,153 @@
/*
* 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 javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.io.Closeable;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming
* instances of this connection will be produced by the LdapConnectionFactory.bind() methods.
*
* A standard looking usage pattern could look like this:
<pre>
try (LdapConnection session = ldapFac.bindXXX(...);
...do stuff with the session
}
</pre>
*/
public class LdapConnection implements Closeable {
private final String bindDn;
private final DirContext ldapContext;
private final String groupSearchDN;
private final boolean isGroupSubTreeSearch;
private final boolean isFindGroupsByAttribute;
/**
* This object is intended to be constructed by the LdapConnectionFactory
*/
LdapConnection(DirContext ctx, String boundName, boolean isFindGroupsByAttribute, boolean isGroupSubTreeSearch, String groupSearchDN) {
this.ldapContext = ctx;
this.bindDn = boundName;
this.isGroupSubTreeSearch = isGroupSubTreeSearch;
this.groupSearchDN = groupSearchDN;
this.isFindGroupsByAttribute = isFindGroupsByAttribute;
}
/**
* 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.
*/
public void close(){
try {
ldapContext.close();
} catch (NamingException e) {
throw new SecurityException("Could not close the LDAP connection", e);
}
}
/**
* Fetches the groups that the user is a member of
* @return List of group membership
*/
public List<String> getGroups(){
return isFindGroupsByAttribute ? getGroupsFromUserAttrs(bindDn) : getGroupsFromSearch(bindDn);
}
/**
* Fetches the groups of a user by doing a search. This could be abstracted out into a strategy class or through
* an inherited class (with getGroups as the template method).
* @param userDn user fully distinguished name to fetch group membership for
* @return fully distinguished names of the roles
*/
List<String> getGroupsFromSearch(String userDn){
List<String> groups = new LinkedList<>();
SearchControls search = new SearchControls();
search.setReturningAttributes( new String[0] );
search.setSearchScope( this.isGroupSubTreeSearch ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE);
//This could be made could be made configurable but it should cover all cases
String filter = "(&" +
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group)) " +
"(|(uniqueMember={0})(member={0})))";
try {
NamingEnumeration<SearchResult> results = ldapContext.search(
groupSearchDN, filter, new Object[]{userDn}, search);
while (results.hasMoreElements()){
groups.add(results.next().getNameInNamespace());
}
} catch (NamingException e) {
throw new SecurityException("Could not search for an LDAP group for user [" + userDn + "]", e);
}
return groups;
}
/**
* Fetches the groups from the user attributes (if supported). This method could later be abstracted out
* into a strategy class
* @param userDn User fully distinguished name to fetch group membership from
* @return list of groups the user is a member of.
*/
List<String> getGroupsFromUserAttrs(String userDn) {
List<String> groupDns = new LinkedList<>();
try {
Attributes results = ldapContext.getAttributes(userDn, new String[]{"memberOf", "isMemberOf"});
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
Attribute attr = (Attribute)ae.next();
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
Object val = attrEnum.next();
if (val instanceof String) {
String stringVal = (String) val;
groupDns.add(stringVal);
}
}
}
} catch (NamingException e) {
throw new SecurityException("Could not look up group attributes for user [" + userDn + "]", e);
}
return groupDns;
}
/**
* Fetches common user attributes from the user. Its a good way to ensure a connection works.
*/
public Map<String,String[]> getUserAttrs(String userDn) {
Map <String, String[]>userAttrs = new HashMap<>();
try {
Attributes results = ldapContext.getAttributes(userDn, new String[]{"uid", "memberOf", "isMemberOf"});
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
Attribute attr = (Attribute)ae.next();
LinkedList<String> attrList = new LinkedList<>();
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
Object val = attrEnum.next();
if (val instanceof String) {
String stringVal = (String) val;
attrList.add(stringVal);
}
}
String[] attrArray = attrList.toArray(new String[attrList.size()]);
userAttrs.put(attr.getID(), attrArray);
}
} catch (NamingException e) {
throw new SecurityException("Could not look up attributes for user [" + userDn + "]", e);
}
return userAttrs;
}
public String getAuthenticatedUserDn() {
return bindDn;
}
}

View File

@ -0,0 +1,29 @@
/*
* 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;
/**
* 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>
try (LdapConnection session = ldapFac.bindXXX(...);
...do stuff with the session
}
</pre>
*/
public interface LdapConnectionFactory {
public static final String URLS_SETTING = "urls"; //comma separated
/**
* Password authenticated bind
* @param user name of the user to authenticate the connection with.
*/
public LdapConnection bind(String user, char[] password) ;
}

View File

@ -0,0 +1,27 @@
/*
* 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;
/**
* LdapExceptions typically wrap jndi Naming exceptions, and have an additional
* parameter of DN attached to each message.
*/
public class LdapException extends SecurityException {
public LdapException(String msg){
super(msg);
}
public LdapException(String msg, Throwable cause){
super(msg, cause);
}
public LdapException(String msg, String dn) {
this(msg, dn, null);
}
public LdapException(String msg, String dn, Throwable cause) {
super( msg + "; LDAP DN=[" + dn + "]", cause);
}
}

View File

@ -0,0 +1,153 @@
/*
* 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.ElasticsearchException;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
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;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
/**
* This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles.
*/
public class LdapGroupToRoleMapper extends AbstractComponent {
public static final String ROLE_MAPPING_DEFAULT_FILE_NAME = ".role_mapping";
public static final String ROLE_MAPPING_FILE_SETTING = "files.role_mapping";
public static final String USE_UNMAPPED_GROUPS_AS_ROLES_SETTING = "unmapped_groups_as_roles";
private final Path file;
private final Listener listener;
private final boolean useUnmappedGroupsAsRoles;
private volatile ImmutableMap<LdapName, Set<String>> groupRoles;
@Inject
public LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, Listener.NOOP);
}
LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) {
super(settings);
useUnmappedGroupsAsRoles = componentSettings.getAsBoolean(USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, false);
file = resolveFile(componentSettings, env);
groupRoles = parseFile(file, logger);
FileWatcher watcher = new FileWatcher(file.getParent().toFile());
watcher.addListener(new FileListener());
watcherService.add(watcher);
this.listener = listener;
}
public static ImmutableMap<LdapName, Set<String>> parseFile(Path path, ESLogger logger) {
if (!Files.exists(path)) {
return ImmutableMap.of();
}
try (FileInputStream in = new FileInputStream( path.toFile() )){
Settings settings = ImmutableSettings.builder()
.loadFromStream(path.toString(), in)
.build();
Map<LdapName, 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);
Set<String> groupRoles = groupToRoles.get(group);
if (groupRoles == null){
groupRoles = new HashSet<>();
groupToRoles.put(group, groupRoles);
}
groupRoles.add(role);
} catch (InvalidNameException e) {
logger.error("Invalid group DN [{}] found in ldap group to role mappings [{}]. Skipping... ", e, ldapDN, path);
}
}
}
return ImmutableMap.copyOf(groupToRoles);
} catch (IOException e) {
throw new ElasticsearchException("unable to load ldap role mapper file [" + path.toAbsolutePath() + "]", e);
}
}
public static Path resolveFile(Settings settings, Environment env) {
String location = settings.get(ROLE_MAPPING_FILE_SETTING);
if (location == null) {
return env.configFile().toPath().resolve(ROLE_MAPPING_DEFAULT_FILE_NAME);
}
return Paths.get(location);
}
/**
* This will map the groupDN's to ES Roles
*/
public Set<String> mapRoles(List<String> groupDns) {
Set<String>roles = new HashSet<>();
for(String groupDn: groupDns){
LdapName groupLdapName = LdapUtils.ldapName(groupDn);
if (this.groupRoles.containsKey(groupLdapName)) {
roles.addAll(this.groupRoles.get(groupLdapName));
} else if (useUnmappedGroupsAsRoles) {
roles.add(getRelativeName(groupLdapName));
}
}
return roles;
}
String getRelativeName(LdapName groupLdapName) {
return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue();
}
private class FileListener extends FileChangesListener {
@Override
public void onFileCreated(File file) {
onFileChanged(file);
}
@Override
public void onFileDeleted(File file) {
onFileChanged(file);
}
@Override
public void onFileChanged(File file) {
if (file.equals(LdapGroupToRoleMapper.this.file.toFile())) {
groupRoles = parseFile(file.toPath(), logger);
listener.onRefresh();
}
}
}
public static interface Listener {
static final Listener NOOP = new Listener() {
@Override
public void onRefresh() {
}
};
void onRefresh();
}
}

View File

@ -9,9 +9,14 @@ import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
/** /**
* * Configures Ldap object injections
*/ */
public class LdapModule extends AbstractModule { public class LdapModule extends AbstractModule {
private final Settings settings;
public LdapModule(Settings settings) {
this.settings = settings;
}
public static boolean enabled(Settings settings) { public static boolean enabled(Settings settings) {
Settings ldapSettings = settings.getComponentSettings(LdapModule.class); Settings ldapSettings = settings.getComponentSettings(LdapModule.class);
@ -20,6 +25,13 @@ public class LdapModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {
bind(LdapRealm.class).asEagerSingleton();
bind(LdapGroupToRoleMapper.class).asEagerSingleton();
String mode = settings.getComponentSettings(LdapModule.class).get("mode", "ldap");
if ("ldap".equals(mode)) {
bind(LdapConnectionFactory.class).to(StandardLdapConnectionFactory.class);
} else {
bind(LdapConnectionFactory.class).to(ActiveDirectoryConnectionFactory.class);
}
} }
} }

View File

@ -5,25 +5,36 @@
*/ */
package org.elasticsearch.shield.authc.ldap; package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.User; import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken; import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.SecurityException;
import org.elasticsearch.shield.authc.Realm; import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.transport.TransportMessage; import org.elasticsearch.transport.TransportMessage;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/** /**
* * Authenticates username/password tokens against ldap, locates groups and maps them to roles.
*/ */
public class LdapRealm extends AbstractComponent implements Realm<UsernamePasswordToken> { public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<UsernamePasswordToken> {
private static final String TYPE = "ldap"; private static final String TYPE = "ldap";
private final LdapConnectionFactory connectionFactory;
private final LdapGroupToRoleMapper roleMapper;
@Inject @Inject
public LdapRealm(Settings settings) { public LdapRealm(Settings settings, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper) {
super(settings); super(settings);
this.connectionFactory = ldap;
this.roleMapper = roleMapper;
} }
@Override @Override
@ -36,13 +47,26 @@ public class LdapRealm extends AbstractComponent implements Realm<UsernamePasswo
return UsernamePasswordToken.extractToken(message, null); return UsernamePasswordToken.extractToken(message, null);
} }
@Override
public boolean supports(AuthenticationToken token) { public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken; return token instanceof UsernamePasswordToken;
} }
/**
* Given a username and password, connect to ldap, retrieve groups, map to roles and build the user.
* @return User with elasticsearch roles
*/
@Override @Override
public User authenticate(UsernamePasswordToken token) { protected User doAuthenticate(UsernamePasswordToken token) {
try (LdapConnection session = connectionFactory.bind(token.principal(), token.credentials())) {
List<String> groupDNs = session.getGroups();
Set<String> roles = roleMapper.mapRoles(groupDNs);
User.Simple user = new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));
Arrays.fill(token.credentials(), '\0');
return user;
} catch (SecurityException e){
logger.info("Authentication Failed for user [{}]", e, token.principal());
return null; return null;
} }
}
} }

View File

@ -0,0 +1,25 @@
/*
* 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.ElasticsearchException;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
public class LdapUtils {
private LdapUtils() {
}
public static LdapName ldapName(String dn) {
try {
return new LdapName(dn);
} catch (InvalidNameException e) {
throw new ElasticsearchException("Invalid group DN is set [" + dn + "]", e);
}
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
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 java.text.MessageFormat;
import java.util.Hashtable;
/**
* This factory creates LDAP connections via iterating through user templates.
*
* 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 StandardLdapConnectionFactory extends AbstractComponent implements LdapConnectionFactory {
public static final String USER_DN_TEMPLATES_SETTING = "user_dn_templates";
public static final String GROUP_SEARCH_SUBTREE_SETTING = "group_search.subtree_search";
public static final String GROUP_SEARCH_BASEDN_SETTING = "group_search.group_search_dn";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String[] userDnTemplates;
protected final String groupSearchDN;
protected final boolean groupSubTreeSearch;
protected final boolean findGroupsByAttribute;
@Inject
public StandardLdapConnectionFactory(Settings settings) {
super(settings);
userDnTemplates = componentSettings.getAsArray(USER_DN_TEMPLATES_SETTING);
if (userDnTemplates == null) {
throw new org.elasticsearch.shield.SecurityException("Missing required ldap setting [" + USER_DN_TEMPLATES_SETTING + "]");
}
String[] ldapUrls = componentSettings.getAsArray(URLS_SETTING);
if (ldapUrls == null) {
throw new org.elasticsearch.shield.SecurityException("Missing required ldap setting [" + URLS_SETTING + "]");
}
sharedLdapEnv = ImmutableMap.<String, Serializable>builder()
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
.put(Context.REFERRAL, "follow")
.build();
groupSearchDN = componentSettings.get(GROUP_SEARCH_BASEDN_SETTING);
findGroupsByAttribute = groupSearchDN == null;
groupSubTreeSearch = componentSettings.getAsBoolean(GROUP_SEARCH_SUBTREE_SETTING, false);
}
/**
* This iterates through the configured user templates attempting to connect. If all attempts fail, all exceptions
* are combined into one Exception as nested exceptions.
* @param username a relative name, Not a distinguished name, that will be inserted into the template.
* @return authenticated exception
*/
@Override
public LdapConnection bind(String username, char[] password) {
//SASL, MD5, etc. all options here stink, we really need to go over ssl + simple authentication
Hashtable<String, java.io.Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_CREDENTIALS, password);
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(ctx, dn, findGroupsByAttribute, groupSubTreeSearch, groupSearchDN);
} catch (NamingException e) {
logger.warn("Failed ldap authentication with user template [{}], dn [{}]", template, dn);
}
}
throw new LdapException("Failed ldap authentication");
}
/**
* Securely escapes the username and inserts it into the template using MessageFormat
* @param username username to insert into the DN template. Any commas, equals or plus will be escaped.
* @return DN (distinquished name) build from the template.
*/
String buildDnFromTemplate(String username, String template) {
//this value must be escaped to avoid manipulation of the template DN.
String escapedUsername = Rdn.escapeValue(username);
return MessageFormat.format(template, escapedUsername);
}
}

View File

@ -24,16 +24,18 @@ public abstract class CachingUsernamePasswordRealm extends AbstractComponent imp
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1); private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1);
private static final int DEFAULT_MAX_USERS = 100000; //100k users private static final int DEFAULT_MAX_USERS = 100000; //100k users
public static final String CACHE_TTL = "cache.ttl";
public static final String CACHE_MAX_USERS = "cache.max_users";
private final Cache<String, UserWithHash> cache; private final Cache<String, UserWithHash> cache;
protected CachingUsernamePasswordRealm(Settings settings) { protected CachingUsernamePasswordRealm(Settings settings) {
super(settings); super(settings);
TimeValue ttl = componentSettings.getAsTime("cache.ttl", DEFAULT_TTL); TimeValue ttl = componentSettings.getAsTime(CACHE_TTL, DEFAULT_TTL);
if (ttl.millis() > 0) { if (ttl.millis() > 0) {
cache = CacheBuilder.newBuilder() cache = CacheBuilder.newBuilder()
.expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS) .expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS)
.maximumSize(settings.getAsInt("cache.max_users", DEFAULT_MAX_USERS)) .maximumSize(settings.getAsInt(CACHE_MAX_USERS, DEFAULT_MAX_USERS))
.build(); .build();
} else { } else {
cache = null; cache = null;

View File

@ -0,0 +1,69 @@
/*
* 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.test.ElasticsearchTestCase;
import org.junit.Ignore;
import org.junit.Test;
import java.util.List;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
public static final String OPEN_LDAP_URL = "ldap://ad.es.com:389";
public static final String AD_LDAP_URL = "ldap://54.213.145.20:389";
public static final String PASSWORD = "4joD8LmWcrEfRa&p";
public static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
@Ignore
@Test
public void testAdAuth() {
ActiveDirectoryConnectionFactory connectionFactory = new ActiveDirectoryConnectionFactory(
buildAdSettings(AD_LDAP_URL, "ad.test.elasticsearch.com"));
String userName = "ironman";
LdapConnection ldap = connectionFactory.bind(userName, PASSWORD.toCharArray());
String userDN = ldap.getAuthenticatedUserDn();
//System.out.println("userPassword check:"+ldap.checkPassword(userDn, userPass));
List<String> groups = ldap.getGroupsFromUserAttrs(userDN);
System.out.println("groups: "+groups);
}
@Ignore
@Test
public void testAD_standardLdapConnection(){
String groupSearchBase = "dc=ad,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "cn={0},cn=Users,dc=ad,dc=test,dc=elasticsearch,dc=com";
boolean isSubTreeSearch = true;
StandardLdapConnectionFactory connectionFactory = new StandardLdapConnectionFactory(
LdapConnectionTests.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Tony Stark";
LdapConnection ldap = connectionFactory.bind(user, PASSWORD.toCharArray());
List<String> groups = ldap.getGroupsFromUserAttrs(ldap.getAuthenticatedUserDn());
List<String> groups2 = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
assertThat(groups, containsInAnyOrder(containsString("upchuckers"), containsString("localDistribution")));
assertThat(groups2, containsInAnyOrder(containsString("upchuckers"), containsString("localDistribution")));
}
public static Settings buildAdSettings(String ldapUrl, String adDomainName) {
return ImmutableSettings.builder()
.putArray(SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.URLS_SETTING, ldapUrl)
.put(SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomainName)
.build();
}
}

View File

@ -0,0 +1,303 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import com.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. Here is a typical use example in tests:
* <pre>
* static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif");
*
* @BeforeClass public static void startServer() throws Exception {
* ldap.startServer();
* }
* @AfterClass public static void stopServer() throws Exception {
* ldap.stopAndCleanup();
* }
* </pre>
*/
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, String testName) {
this.workDir = new File(System.getProperty("java.io.tmpdir") + "/server-work/" + testName);
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();
workDir.delete();
}
/**
* 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);
}
// We are all done !
}
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();
}
/**
* Main class.
*
* @param args Not used.
*/
public static void main(String[] args) {
try {
String baseDir = "o=sevenSeas";
String ldifImport = "seven-seas.ldif";
// Create the server
ApacheDsEmbedded ads = new ApacheDsEmbedded(baseDir, ldifImport, "test");
ads.startServer();
// Read an entry
Entry result = ads.service.getAdminSession().lookup(new Dn(baseDir));
// And print it if available
System.out.println("Found entry : " + result);
// optionally we can start a server too
ads.stopAndCleanup();
} catch (Exception e) {
// Ok, we have something wrong going on ...
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import static org.hamcrest.Matchers.*;
public class LdapConnectionTests extends ElasticsearchTestCase {
public static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", LdapConnectionTests.class.getName());
@BeforeClass
public static void startServer() throws Exception {
ldap.startServer();
}
@AfterClass
public static void stopServer() throws Exception {
ldap.stopAndCleanup();
}
@Test
public void testBindWithTemplates() {
String[] ldapUrls = new String[]{ldap.getUrl()};
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
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
};
StandardLdapConnectionFactory connectionFactory = new StandardLdapConnectionFactory(
buildLdapSettings(ldapUrls, userTemplates, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
char[] userPass = "pass".toCharArray();
LdapConnection ldap = connectionFactory.bind(user, userPass);
Map<String, String[]> attrs = ldap.getUserAttrs(ldap.getAuthenticatedUserDn());
assertThat( attrs, hasKey("uid"));
assertThat( attrs.get("uid"), arrayContaining("hhornblo"));
}
@Test
public void testBindWithBogusTemplates() {
String[] ldapUrl = new String[]{ldap.getUrl()};
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
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
};
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
char[] userPass = "pass".toCharArray();
try {
LdapConnection ldap = ldapFac.bind(user, userPass);
fail("bindWithUserTemplates should have failed");
} catch (LdapException le) {
}
}
@Test
public void testGroupLookup_Subtree() {
String groupSearchBase = "o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
boolean isSubTreeSearch = true;
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
char[] userPass = "pass".toCharArray();
LdapConnection ldap = ldapFac.bind(user, userPass);
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
System.out.println("groups:"+groups);
assertThat(groups, contains("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";
boolean isSubTreeSearch = false;
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
LdapConnection ldap = ldapFac.bind(user, "pass".toCharArray());
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
System.out.println("groups:"+groups);
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
}
public static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
return buildLdapSettings( new String[]{ldapUrl}, new String[]{userTemplate}, groupSearchBase, isSubTreeSearch );
}
public static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
return ImmutableSettings.builder()
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.URLS_SETTING, ldapUrl)
.putArray(SETTINGS_PREFIX + StandardLdapConnectionFactory.USER_DN_TEMPLATES_SETTING, userTemplate)
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_BASEDN_SETTING, groupSearchBase)
.put(SETTINGS_PREFIX + StandardLdapConnectionFactory.GROUP_SEARCH_SUBTREE_SETTING, isSubTreeSearch).build();
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Set;
import static org.hamcrest.Matchers.hasItems;
public class LdapGroupToRoleMapperTest extends ElasticsearchTestCase {
private final String tonyStarkDN = "cn=tstark,ou=marvel,o=superheros";
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"
};
private final String roleShield = "shield";
private final String roleAvenger = "avenger";
@Test
public void testYaml() throws IOException {
File file = this.getResource("role_mapping.yml");
Settings settings = ImmutableSettings.settingsBuilder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.ROLE_MAPPING_FILE_SETTING, file.getCanonicalPath())
.build();
LdapGroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
new Environment(settings),
new ResourceWatcherService(settings, new ThreadPool("test")));
Set<String> roles = mapper.mapRoles( Arrays.asList(starkGroupDns) );
//verify
assertThat(roles, hasItems(roleShield, roleAvenger));
}
@Test
public void testRelativeDN() {
Settings settings = ImmutableSettings.builder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
LdapGroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
new Environment(settings),
new ResourceWatcherService(settings, new ThreadPool("test")));
Set<String> roles = mapper.mapRoles(Arrays.asList(starkGroupDns));
assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers"));
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import static org.hamcrest.Matchers.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;
public class LdapRealmTest extends ElasticsearchTestCase {
static ApacheDsEmbedded ldap = new ApacheDsEmbedded("o=sevenSeas", "seven-seas.ldif", LdapRealmTest.class.getName());
public static String AD_IP = "54.213.145.20";
public static String AD_URL = "ldap://" + AD_IP + ":389";
public static final String VALID_USER_TEMPLATE = "cn={0},ou=people,o=sevenSeas";
public static final String VALID_USERNAME = "Thomas Masterman Hardy";
public static final String PASSWORD = "pass";
@BeforeClass
public static void startServer() throws Exception {
ldap.startServer();
}
@AfterClass
public static void stopServer() throws Exception {
ldap.stopAndCleanup();
}
@Test
public void testAuthenticate_subTreeGroupSearch(){
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch);
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(settings);
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
assertThat( user, notNullValue());
assertThat(user.roles(), arrayContaining("HMS Victory"));
}
@Test
public void testAuthenticate_oneLevelGroupSearch(){
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
boolean isSubTreeSearch = false;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
assertThat( user, notNullValue());
assertThat( user.roles(), arrayContaining("HMS Victory"));
}
@Ignore //this is still failing. not sure why.
@Test
public void testAuthenticate_caching(){
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
LdapConnectionTests.buildLdapSettings( ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm( buildCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
//verify one and only one bind -> caching is working
verify(ldapFactory, times(1)).bind(anyString(), any(char[].class));
}
@Test
public void testAuthenticate_noncaching(){
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
LdapConnectionTests.buildLdapSettings(ldap.getUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
ldapFactory = spy(ldapFactory);
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
user = ldap.authenticate( new UsernamePasswordToken(VALID_USERNAME, PASSWORD.toCharArray()));
//verify two and only two binds -> caching is disabled
verify(ldapFactory, times(2)).bind(anyString(), any(char[].class));
}
@Ignore
@Test
public void testAD() {
String adDomain = "ad.test.elasticsearch.com";
String userSearchBaseDN = "dc=ad,dc=es,dc=com";
ActiveDirectoryConnectionFactory ldapFactory = new ActiveDirectoryConnectionFactory(
ActiveDirectoryFactoryTests.buildAdSettings(AD_URL, adDomain));
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate( new UsernamePasswordToken("george", "R))Tr0x".toCharArray()));
assertThat( user, notNullValue());
assertThat( user.roles(), hasItemInArray("upchuckers"));
}
@Ignore
@Test
public void testAD_defaults() {
//only set the adDomain, and see if it infers the rest correctly
String adDomain = AD_IP;
Settings settings = ImmutableSettings.builder()
.put(LdapConnectionTests.SETTINGS_PREFIX + ActiveDirectoryConnectionFactory.AD_DOMAIN_NAME_SETTING, adDomain)
.build();
ActiveDirectoryConnectionFactory ldapFactory = new ActiveDirectoryConnectionFactory( settings );
LdapRealm ldap = new LdapRealm( buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper());
User user = ldap.authenticate( new UsernamePasswordToken("george", "R))Tr0x".toCharArray()));
assertThat( user, notNullValue());
assertThat( user.roles(), hasItemInArray("upchuckers"));
}
private Settings buildNonCachingSettings() {
return ImmutableSettings.builder()
.put("shield.authc.ldap."+LdapRealm.CACHE_TTL, -1)
.build();
}
private Settings buildCachingSettings() {
return ImmutableSettings.builder()
.put("shield.authc.ldap."+LdapRealm.CACHE_TTL, 1)
.put("shield.authc.ldap."+LdapRealm.CACHE_MAX_USERS, 10)
.build();
}
private LdapGroupToRoleMapper buildGroupAsRoleMapper() {
Settings settings = ImmutableSettings.builder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
return new LdapGroupToRoleMapper(settings,
new Environment(settings),
new ResourceWatcherService(settings, new ThreadPool("test")));
}
}

View File

@ -0,0 +1,11 @@
# This LDAP connection does group lookup by a subtree search, no role mapping
ldap:
urls:
- ldap://ldap.example.com:1389
userDnTemplate: "cn={0},ou=people,o=superheros"
groupSearch:
isSubtreeSearch: "true"
baseDn: "ou=marvel,o=superheros"
# no dnToRoleMapping mapping implies that the group name will be used directly as the ES role name

View File

@ -0,0 +1,10 @@
# This LDAP connection does group lookup by attribute with group to role mapping
ldap:
urls: # these connections are not round-robin, but primary, secondary, etc. When the first fails the second is attempted
- ldap://ldap.example.com:1389
userDnTemplate: "cn={0},ou=people,o=superheros"
# dnToRoleMapping as true means that group will be mapped to
# local ES roles using
dnToRoleMapping: true
#roleMappingFile: /etc/es/role_mapping.yaml

View File

@ -0,0 +1,5 @@
shield:
- "cn=avengers,ou=marvel,o=superheros"
- "cn=shield,ou=marvel,o=superheros"
avenger:
- "cn=avengers,ou=marvel,o=superheros"

View File

@ -0,0 +1,219 @@
# Sample LDIF data for the ApacheDS v1.0 Basic User's Guide
#
# Some sailors and their ships
# userpassword for all persons is "pass"
#
version: 1
dn: ou=people,o=sevenSeas
objectclass: organizationalUnit
objectclass: top
description: Contains entries which describe persons (seamen)
ou: people
dn: ou=groups,o=sevenSeas
objectclass: organizationalUnit
objectclass: top
description: Contains entries which describe groups (crews, for instance)
ou: groups
dn: ou=crews,ou=groups,o=sevenSeas
objectclass: organizationalUnit
objectclass: top
description: Contains entries which describe ship crews
ou: crews
dn: ou=ranks,ou=groups,o=sevenSeas
objectclass: organizationalUnit
objectclass: top
description: Contains entries which describe naval ranks (e.g. captain)
ou: ranks
# HMS Lydia Crew
# --------------
dn: cn=Horatio Hornblower,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Horatio Hornblower
description: Capt. Horatio Hornblower, R.N
givenname: Horatio
sn: Hornblower
uid: hhornblo
mail: hhornblo@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=William Bush,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: William Bush
description: Lt. William Bush
givenname: William
manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Bush
uid: wbush
mail: wbush@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=Thomas Quist,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Thomas Quist
description: Seaman Quist
givenname: Thomas
manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Quist
uid: tquist
mail: tquist@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=Moultrie Crystal,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Moultrie Crystal
description: Lt. Crystal
givenname: Moultrie
manager: cn=Horatio Hornblower,ou=people,o=sevenSeas
sn: Crystal
uid: mchrysta
mail: mchrysta@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames
objectclass: top
cn: HMS Lydia
uniquemember: cn=Horatio Hornblower,ou=people,o=sevenSeas
uniquemember: cn=William Bush,ou=people,o=sevenSeas
uniquemember: cn=Thomas Quist,ou=people,o=sevenSeas
uniquemember: cn=Moultrie Crystal,ou=people,o=sevenSeas
# HMS Victory Crew
# ----------------
dn: cn=Horatio Nelson,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Horatio Nelson
description: Lord Horatio Nelson
givenname: Horatio
sn: Nelson
uid: hnelson
mail: hnelson@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=Thomas Masterman Hardy,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Thomas Masterman Hardy
description: Sir Thomas Masterman Hardy
givenname: Thomas
manager: cn=Horatio Nelson,ou=people,o=sevenSeas
sn: Hardy
uid: thardy
mail: thardy@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=Cornelius Buckley,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Cornelius Buckley
description: LM Cornelius Buckley
givenname: Cornelius
manager: cn=Horatio Nelson,ou=people,o=sevenSeas
sn: Buckley
uid: cbuckley
mail: cbuckley@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=HMS Victory,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames
objectclass: top
cn: HMS Victory
uniquemember: cn=Horatio Nelson,ou=people,o=sevenSeas
uniquemember: cn=Thomas Masterman Hardy,ou=people,o=sevenSeas
uniquemember: cn=Cornelius Buckley,ou=people,o=sevenSeas
# HMS Bounty Crew
# ---------------
dn: cn=William Bligh,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: William Bligh
description: Captain William Bligh
givenname: William
sn: Bligh
uid: wbligh
mail: wbligh@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=Fletcher Christian,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: Fletcher Christian
description: Lieutenant Fletcher Christian
givenname: Fletcher
manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Christian
uid: fchristi
mail: fchristi@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=John Fryer,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: John Fryer
description: Master John Fryer
givenname: John
manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Fryer
uid: jfryer
mail: jfryer@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=John Hallett,ou=people,o=sevenSeas
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: top
cn: John Hallett
description: Midshipman John Hallett
givenname: John
manager: cn=William Bligh,ou=people,o=sevenSeas
sn: Hallett
uid: jhallett
mail: jhallett@royalnavy.mod.uk
userpassword: {SHA}nU4eI71bcnBGqeO0t9tXvY1u5oQ=
dn: cn=HMS Bounty,ou=crews,ou=groups,o=sevenSeas
objectclass: groupOfUniqueNames
objectclass: top
cn: HMS Bounty
uniquemember: cn=William Bligh,ou=people,o=sevenSeas
uniquemember: cn=Fletcher Christian,ou=people,o=sevenSeas
uniquemember: cn=John Fryer,ou=people,o=sevenSeas
uniquemember: cn=John Hallett,ou=people,o=sevenSeas

View File

@ -31,4 +31,12 @@ grant {
// Needed for accept all ssl certs in tests // Needed for accept all ssl certs in tests
permission javax.net.ssl.SSLPermission "setHostnameVerifier"; permission javax.net.ssl.SSLPermission "setHostnameVerifier";
// Needed to startup embedded apacheDS LDAP server for tests
permission java.security.SecurityPermission "putProviderProperty.BC";
permission java.security.SecurityPermission "insertProvider.BC";
permission java.security.SecurityPermission "getProperty.ssl.KeyManagerFactory.algorithm";
//this shouldn't be in a production environment, just to run tests:
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.lang.RuntimePermission "setDefaultUncaughtExceptionHandler";
}; };