From 9cd397727f4d3ece93521e918a88a4f2bc474257 Mon Sep 17 00:00:00 2001 From: c-a-m Date: Tue, 22 Jul 2014 10:06:54 -0600 Subject: [PATCH] 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@ce20e1b3beba5cd9d28b662f576fa17f62705fed --- pom.xml | 18 ++ .../shield/authc/AuthenticationModule.java | 2 +- .../ActiveDirectoryConnectionFactory.java | 107 +++++++ .../shield/authc/ldap/LdapConnection.java | 153 +++++++++ .../authc/ldap/LdapConnectionFactory.java | 29 ++ .../shield/authc/ldap/LdapException.java | 27 ++ .../authc/ldap/LdapGroupToRoleMapper.java | 153 +++++++++ .../shield/authc/ldap/LdapModule.java | 16 +- .../shield/authc/ldap/LdapRealm.java | 38 ++- .../shield/authc/ldap/LdapUtils.java | 25 ++ .../ldap/StandardLdapConnectionFactory.java | 102 ++++++ .../support/CachingUsernamePasswordRealm.java | 6 +- .../ldap/ActiveDirectoryFactoryTests.java | 69 ++++ .../shield/authc/ldap/ApacheDsEmbedded.java | 303 ++++++++++++++++++ .../authc/ldap/LdapConnectionTests.java | 127 ++++++++ .../authc/ldap/LdapGroupToRoleMapperTest.java | 70 ++++ .../shield/authc/ldap/LdapRealmTest.java | 167 ++++++++++ .../authc/ldap/ldapWithGroupSearch.yaml | 11 + .../authc/ldap/ldapWithRoleMapping.yaml | 10 + .../shield/authc/ldap/role_mapping.yml | 5 + .../shield/authc/ldap/seven-seas.ldif | 219 +++++++++++++ tests.policy | 8 + 22 files changed, 1653 insertions(+), 12 deletions(-) create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryConnectionFactory.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/LdapException.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapper.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/LdapUtils.java create mode 100644 src/main/java/org/elasticsearch/shield/authc/ldap/StandardLdapConnectionFactory.java create mode 100644 src/test/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryFactoryTests.java create mode 100644 src/test/java/org/elasticsearch/shield/authc/ldap/ApacheDsEmbedded.java create mode 100644 src/test/java/org/elasticsearch/shield/authc/ldap/LdapConnectionTests.java create mode 100644 src/test/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapperTest.java create mode 100644 src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java create mode 100644 src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithGroupSearch.yaml create mode 100644 src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithRoleMapping.yaml create mode 100644 src/test/resources/org/elasticsearch/shield/authc/ldap/role_mapping.yml create mode 100644 src/test/resources/org/elasticsearch/shield/authc/ldap/seven-seas.ldif diff --git a/pom.xml b/pom.xml index f150c7ac35a..272cd9e0f6e 100644 --- a/pom.xml +++ b/pom.xml @@ -89,6 +89,24 @@ 17.0 test + + org.slf4j + slf4j-log4j12 + 1.7.7 + test + + + org.apache.directory.server + apacheds-all + 2.0.0-M17 + test + + + javax + javaee-api + 6.0 + test + org.mockito mockito-core diff --git a/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java b/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java index befa977ebf7..5614bf46434 100644 --- a/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java +++ b/src/main/java/org/elasticsearch/shield/authc/AuthenticationModule.java @@ -33,7 +33,7 @@ public class AuthenticationModule extends AbstractModule implements SpawnModules modules.add(new ESUsersModule()); } if (LdapModule.enabled(settings)) { - modules.add(new LdapModule()); + modules.add(new LdapModule(settings)); } return modules.build(); } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryConnectionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryConnectionFactory.java new file mode 100644 index 00000000000..20c83cfdd40 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryConnectionFactory.java @@ -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 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.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 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 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="); + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java new file mode 100644 index 00000000000..4a06274170e --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnection.java @@ -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: +
+     try (LdapConnection session = ldapFac.bindXXX(...);
+     ...do stuff with the session
+     }
+     
+ */ +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 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 getGroupsFromSearch(String userDn){ + List 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 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 getGroupsFromUserAttrs(String userDn) { + List 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 getUserAttrs(String userDn) { + Map 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 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; + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java new file mode 100644 index 00000000000..91c2a6278f4 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapConnectionFactory.java @@ -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: +
+    try (LdapConnection session = ldapFac.bindXXX(...);
+        ...do stuff with the session
+    }
+ 
+ */ +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) ; + +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapException.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapException.java new file mode 100644 index 00000000000..ce33df9739b --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapException.java @@ -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); + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapper.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapper.java new file mode 100644 index 00000000000..84196ca21ed --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapper.java @@ -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> 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> 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> groupToRoles = new HashMap<>(); + Set roles = settings.names(); + for(String role: roles){ + for(String ldapDN: settings.getAsArray(role)){ + try { + LdapName group = new LdapName(ldapDN); + Set 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 mapRoles(List groupDns) { + Setroles = 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(); + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapModule.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapModule.java index 54a23985844..1dc422e6025 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapModule.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapModule.java @@ -9,9 +9,14 @@ import org.elasticsearch.common.inject.AbstractModule; import org.elasticsearch.common.settings.Settings; /** - * + * Configures Ldap object injections */ public class LdapModule extends AbstractModule { + private final Settings settings; + + public LdapModule(Settings settings) { + this.settings = settings; + } public static boolean enabled(Settings settings) { Settings ldapSettings = settings.getComponentSettings(LdapModule.class); @@ -20,6 +25,13 @@ public class LdapModule extends AbstractModule { @Override 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); + } } } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java index f76dbba0ebe..666dd165d97 100644 --- a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapRealm.java @@ -5,25 +5,36 @@ */ package org.elasticsearch.shield.authc.ldap; -import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.shield.User; import org.elasticsearch.shield.authc.AuthenticationToken; +import org.elasticsearch.shield.SecurityException; import org.elasticsearch.shield.authc.Realm; +import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm; import org.elasticsearch.shield.authc.support.UsernamePasswordToken; 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 { +public class LdapRealm extends CachingUsernamePasswordRealm implements Realm { private static final String TYPE = "ldap"; + private final LdapConnectionFactory connectionFactory; + private final LdapGroupToRoleMapper roleMapper; + @Inject - public LdapRealm(Settings settings) { + public LdapRealm(Settings settings, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper) { super(settings); + + this.connectionFactory = ldap; + this.roleMapper = roleMapper; } @Override @@ -36,13 +47,26 @@ public class LdapRealm extends AbstractComponent implements Realm groupDNs = session.getGroups(); + Set 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; + } } + } diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUtils.java b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUtils.java new file mode 100644 index 00000000000..b779369d29b --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/LdapUtils.java @@ -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); + } + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/ldap/StandardLdapConnectionFactory.java b/src/main/java/org/elasticsearch/shield/authc/ldap/StandardLdapConnectionFactory.java new file mode 100644 index 00000000000..537d822a4c5 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/ldap/StandardLdapConnectionFactory.java @@ -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 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.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 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); + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java b/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java index 79a69aa4c91..f126bf0f0d8 100644 --- a/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java +++ b/src/main/java/org/elasticsearch/shield/authc/support/CachingUsernamePasswordRealm.java @@ -24,16 +24,18 @@ public abstract class CachingUsernamePasswordRealm extends AbstractComponent imp private static final TimeValue DEFAULT_TTL = TimeValue.timeValueHours(1); 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 cache; protected CachingUsernamePasswordRealm(Settings settings) { super(settings); - TimeValue ttl = componentSettings.getAsTime("cache.ttl", DEFAULT_TTL); + TimeValue ttl = componentSettings.getAsTime(CACHE_TTL, DEFAULT_TTL); if (ttl.millis() > 0) { cache = CacheBuilder.newBuilder() .expireAfterWrite(ttl.getMillis(), TimeUnit.MILLISECONDS) - .maximumSize(settings.getAsInt("cache.max_users", DEFAULT_MAX_USERS)) + .maximumSize(settings.getAsInt(CACHE_MAX_USERS, DEFAULT_MAX_USERS)) .build(); } else { cache = null; diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryFactoryTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryFactoryTests.java new file mode 100644 index 00000000000..6ccd958c595 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/ActiveDirectoryFactoryTests.java @@ -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 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 groups = ldap.getGroupsFromUserAttrs(ldap.getAuthenticatedUserDn()); + List 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(); + } +} diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/ApacheDsEmbedded.java b/src/test/java/org/elasticsearch/shield/authc/ldap/ApacheDsEmbedded.java new file mode 100644 index 00000000000..3fa8da1f83f --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/ApacheDsEmbedded.java @@ -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: + *
+ * 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();
+ * }
+ * 
+ */ +public class ApacheDsEmbedded { + /** + * The child JVM ordinal of this JVM. Placed by the testing framework. Default is 0 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 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(); + } + } +} diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapConnectionTests.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapConnectionTests.java new file mode 100644 index 00000000000..1a52d2da23e --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapConnectionTests.java @@ -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 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 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 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(); + } + +} diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapperTest.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapperTest.java new file mode 100644 index 00000000000..7546f3c408d --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapGroupToRoleMapperTest.java @@ -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 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 roles = mapper.mapRoles(Arrays.asList(starkGroupDns)); + assertThat(roles, hasItems("genius", "billionaire", "playboy", "philanthropist", "shield", "avengers")); + } +} diff --git a/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java new file mode 100644 index 00000000000..3e1a9ff3f7d --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/ldap/LdapRealmTest.java @@ -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"))); + + } +} diff --git a/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithGroupSearch.yaml b/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithGroupSearch.yaml new file mode 100644 index 00000000000..db70f28232a --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithGroupSearch.yaml @@ -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 + + diff --git a/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithRoleMapping.yaml b/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithRoleMapping.yaml new file mode 100644 index 00000000000..d98f48e90bc --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/authc/ldap/ldapWithRoleMapping.yaml @@ -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 diff --git a/src/test/resources/org/elasticsearch/shield/authc/ldap/role_mapping.yml b/src/test/resources/org/elasticsearch/shield/authc/ldap/role_mapping.yml new file mode 100644 index 00000000000..67e8fc14d9c --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/authc/ldap/role_mapping.yml @@ -0,0 +1,5 @@ +shield: + - "cn=avengers,ou=marvel,o=superheros" + - "cn=shield,ou=marvel,o=superheros" +avenger: + - "cn=avengers,ou=marvel,o=superheros" \ No newline at end of file diff --git a/src/test/resources/org/elasticsearch/shield/authc/ldap/seven-seas.ldif b/src/test/resources/org/elasticsearch/shield/authc/ldap/seven-seas.ldif new file mode 100644 index 00000000000..f9462276e22 --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/authc/ldap/seven-seas.ldif @@ -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 + + + diff --git a/tests.policy b/tests.policy index d206149a6b6..ad06580d8b8 100644 --- a/tests.policy +++ b/tests.policy @@ -31,4 +31,12 @@ grant { // Needed for accept all ssl certs in tests 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"; };