LDAP: splitting AD and LDAP realm

This splits the realm into two so that configuration for both are separate.

Original commit: elastic/x-pack-elasticsearch@782997d54b
This commit is contained in:
c-a-m 2014-11-18 07:54:32 -07:00
parent e8119ec933
commit 32f0f621d5
38 changed files with 642 additions and 473 deletions

View File

@ -24,8 +24,7 @@
"users" : ".esvm-shield-config/users",
"users_roles" : ".esvm-shield-config/users_roles"
},
"ldap" : {
"mode" : "active_directory",
"active_directory" : {
"domain_name" : "ad.test.elasticsearch.com",
"url" : "ldaps://ad.test.elasticsearch.com:636",
"unmapped_groups_as_roles" : "false",

View File

@ -21,7 +21,6 @@
},
"authc": {
"ldap" : {
"mode" : "ldap",
"url" : "ldaps://54.200.235.244:636",
"user_dn_templates": ["uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com"],
"group_search.group_search_dn" : "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com",

View File

@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryModule;
import org.elasticsearch.shield.authc.esusers.ESUsersModule;
import org.elasticsearch.shield.authc.ldap.LdapModule;
import org.elasticsearch.shield.authc.system.SystemRealm;
@ -26,7 +27,9 @@ public class AuthenticationModule extends AbstractShieldModule.Node.Spawn {
return ImmutableList.of(
new SystemRealm.Module(settings),
new ESUsersModule(settings),
new LdapModule(settings));
new LdapModule(settings),
new ActiveDirectoryModule(settings)
);
}
@Override

View File

@ -9,6 +9,7 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.shield.authc.active_directory.ActiveDirectoryRealm;
import org.elasticsearch.shield.authc.esusers.ESUsersRealm;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.shield.authc.system.SystemRealm;
@ -26,7 +27,10 @@ public class Realms {
private final Realm[] realms;
@Inject
public Realms(SystemRealm system, @Nullable ESUsersRealm esusers, @Nullable LdapRealm ldap) {
public Realms(SystemRealm system,
@Nullable ESUsersRealm esusers,
@Nullable LdapRealm ldap,
@Nullable ActiveDirectoryRealm activeDirectory) {
List<Realm> realms = new ArrayList<>();
realms.add(system);
@ -38,6 +42,10 @@ public class Realms {
logger.info("Realm [" + ldap.type() + "] is used");
realms.add(ldap);
}
if (activeDirectory != null) {
logger.info("Realm [" + activeDirectory.type() + "] is used");
realms.add(activeDirectory);
}
this.realms = realms.toArray(new Realm[realms.size()]);
}

View File

@ -3,11 +3,13 @@
* 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;
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.shield.authc.ldap.GenericLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.LdapConnection;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
@ -15,7 +17,7 @@ import javax.naming.directory.*;
import java.util.List;
/**
*
* An Ldap Connection customized for active directory.
*/
public class ActiveDirectoryConnection implements LdapConnection {
private static final ESLogger logger = ESLoggerFactory.getLogger(GenericLdapConnection.class.getName());
@ -23,7 +25,6 @@ public class ActiveDirectoryConnection implements LdapConnection {
protected final DirContext ldapContext;
private final String groupSearchDN;
protected final String groupAttribute = "memberOf";
/**
* This object is intended to be constructed by the LdapConnectionFactory
@ -43,7 +44,7 @@ public class ActiveDirectoryConnection implements LdapConnection {
try {
ldapContext.close();
} catch (NamingException e) {
throw new LdapException("Could not close the LDAP connection", e);
throw new ActiveDirectoryException("Could not close the LDAP connection", e);
}
}
@ -51,6 +52,7 @@ public class ActiveDirectoryConnection implements LdapConnection {
public List<String> getGroups() {
String groupsSearchFilter = buildGroupQuery();
logger.debug("group SID to DN search filter: [{}]", groupsSearchFilter);
// Search for groups the user belongs to in order to get their names
//Create the search controls
@ -58,18 +60,12 @@ public class ActiveDirectoryConnection implements LdapConnection {
//Specify the search scope
groupsSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
groupsSearchCtls.setReturningAttributes(new String[] {}); //we only need the entry DN
//Specify the Base for the search
String groupsSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
//Specify the attributes to return
String groupsReturnedAtts[]={};
groupsSearchCtls.setReturningAttributes(groupsReturnedAtts);
ImmutableList.Builder<String> groups = ImmutableList.<String>builder();
ImmutableList.Builder<String> groups = ImmutableList.builder();
try {
//Search for objects using the filter
NamingEnumeration groupsAnswer = ldapContext.search(groupsSearchBase, groupsSearchFilter.toString(), groupsSearchCtls);
NamingEnumeration groupsAnswer = ldapContext.search(groupSearchDN, groupsSearchFilter, groupsSearchCtls);
//Loop through the search results
while (groupsAnswer.hasMoreElements()) {
@ -77,13 +73,13 @@ public class ActiveDirectoryConnection implements LdapConnection {
groups.add(sr.getNameInNamespace());
}
} catch (NamingException ne) {
throw new LdapException("Exception occurred fetching AD groups", bindDn, ne);
throw new ActiveDirectoryException("Exception occurred fetching AD groups", bindDn, ne);
}
return groups.build();
}
private String buildGroupQuery() {
StringBuffer groupsSearchFilter = new StringBuffer("(|");
StringBuilder groupsSearchFilter = new StringBuilder("(|");
try {
SearchControls userSearchCtls = new SearchControls();
userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
@ -105,7 +101,9 @@ public class ActiveDirectoryConnection implements LdapConnection {
Attribute attr = (Attribute)ae.next();
for (NamingEnumeration e = attr.getAll();e.hasMore();) {
byte[] sid = (byte[])e.next();
groupsSearchFilter.append("(objectSid=" + binarySidToStringSid(sid) + ")");
groupsSearchFilter.append("(objectSid=");
groupsSearchFilter.append(binarySidToStringSid(sid));
groupsSearchFilter.append(")");
}
groupsSearchFilter.append(")");
}
@ -113,7 +111,7 @@ public class ActiveDirectoryConnection implements LdapConnection {
}
} catch (NamingException ne) {
throw new LdapException("Exception occurred fetching AD groups", bindDn, ne);
throw new ActiveDirectoryException("Exception occurred fetching AD groups", bindDn, ne);
}
return groupsSearchFilter.toString();
}
@ -127,10 +125,9 @@ public class ActiveDirectoryConnection implements LdapConnection {
* No idea whats going on here. Its copied from here:
* http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx
* @param SID byte encoded security ID
* @return
*/
public String binarySidToStringSid( byte[] SID ) {
String strSID = "";
String strSID;
//convert the SID into string format

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
@ -12,6 +12,8 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.ShieldException;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.LdapConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
@ -90,13 +92,13 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
if (!results.hasMore()) {
return new ActiveDirectoryConnection(ctx, name, userSearchDN);
}
throw new LdapException("Search for user [" + userName + "] by principle name yielded multiple results");
throw new ActiveDirectoryException("Search for user [" + userName + "] by principle name yielded multiple results");
}
throw new LdapException("Search for user [" + userName + "], search root [" + userSearchDN + "] yielded no results");
throw new ActiveDirectoryException("Search for user [" + userName + "], search root [" + userSearchDN + "] yielded no results");
} catch (NamingException e) {
throw new LdapException("Unable to authenticate user [" + userName + "] to active directory domain ["+ domainName +"]", e);
throw new ActiveDirectoryException("Unable to authenticate user [" + userName + "] to active directory domain ["+ domainName +"]", e);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
/**
* LdapExceptions typically wrap jndi Naming exceptions, and have an additional
* parameter of DN attached to each message.
*/
public class ActiveDirectoryException extends SecurityException {
public ActiveDirectoryException(String msg){
super(msg);
}
public ActiveDirectoryException(String msg, Throwable cause){
super(msg, cause);
}
public ActiveDirectoryException(String msg, String dn) {
this(msg, dn, null);
}
public ActiveDirectoryException(String msg, String dn, Throwable cause) {
super( msg + "; DN=[" + dn + "]", cause);
}
//TODO map active directory error codes to better messages like in this:
// https://github.com/spring-projects/spring-security/blob/master/ldap/src/main/java/org/springframework/security/ldap/authentication/ad/ActiveDirectoryLdapAuthenticationProvider.java#L65
}

View File

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

View File

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.support.AbstractShieldModule;
import static org.elasticsearch.common.inject.name.Names.named;
/**
* Configures ActiveDirectory Realm object injections
*/
public class ActiveDirectoryModule extends AbstractShieldModule.Node {
private final boolean enabled;
public ActiveDirectoryModule(Settings settings) {
super(settings);
enabled = enabled(settings);
}
@Override
protected void configureNode() {
if (enabled) {
/* This socket factory needs to be configured before any LDAP connections are created. LDAP configuration
for JNDI invokes a static getSocketFactory method from LdapSslSocketFactory. */
requestStaticInjection(LdapSslSocketFactory.class);
bind(Realm.class).annotatedWith(named(ActiveDirectoryRealm.type)).to(ActiveDirectoryRealm.class).asEagerSingleton();
} else {
bind(ActiveDirectoryRealm.class).toProvider(Providers.<ActiveDirectoryRealm>of(null));
}
}
static boolean enabled(Settings settings) {
Settings authcSettings = settings.getAsSettings("shield.authc");
if (!authcSettings.names().contains(ActiveDirectoryRealm.type)) {
return false;
}
Settings ldapSettings = authcSettings.getAsSettings(ActiveDirectoryRealm.type);
return ldapSettings.getAsBoolean("enabled", true);
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapRealm;
/**
*
*/
public class ActiveDirectoryRealm extends AbstractLdapRealm {
public static final String type = "active_directory";
@Inject
public ActiveDirectoryRealm(Settings settings,
ActiveDirectoryConnectionFactory connectionFactory,
ActiveDirectoryGroupToRoleMapper roleMapper,
RestController restController) {
super(settings, connectionFactory, roleMapper, restController);
}
@Override
public String type() {
return type;
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapConnection;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
@ -81,7 +82,7 @@ public class GenericLdapConnection implements LdapConnection {
* @param userDn user fully distinguished name to fetch group membership for
* @return fully distinguished names of the roles
*/
List<String> getGroupsFromSearch(String userDn){
public List<String> getGroupsFromSearch(String userDn){
List<String> groups = new LinkedList<>();
SearchControls search = new SearchControls();
search.setReturningAttributes( new String[0] );
@ -110,7 +111,7 @@ public class GenericLdapConnection implements LdapConnection {
* @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) {
public List<String> getGroupsFromUserAttrs(String userDn) {
List<String> groupDns = new LinkedList<>();
try {
Attributes results = ldapContext.getAttributes(userDn, new String[]{groupAttribute});

View File

@ -12,6 +12,8 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.ShieldException;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.LdapConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import javax.naming.Context;
import javax.naming.NamingException;
@ -28,12 +30,11 @@ import java.util.Hashtable;
* Note that even though there is a separate factory for Active Directory, this factory would work against AD. A template
* for each user context would need to be supplied.
*/
public class StandardLdapConnectionFactory extends AbstractComponent implements LdapConnectionFactory {
public class GenericLdapConnectionFactory 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";
static final String MODE_NAME = "ldap";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String[] userDnTemplates;
@ -42,7 +43,7 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
protected final boolean findGroupsByAttribute;
@Inject
public StandardLdapConnectionFactory(Settings settings) {
public GenericLdapConnectionFactory(Settings settings) {
super(settings);
userDnTemplates = componentSettings.getAsArray(USER_DN_TEMPLATES_SETTING);
if (userDnTemplates == null) {

View File

@ -5,169 +5,18 @@
*/
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.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.plugin.ShieldPlugin;
import org.elasticsearch.watcher.FileChangesListener;
import org.elasticsearch.watcher.FileWatcher;
import org.elasticsearch.shield.authc.support.ldap.GroupToRoleMapper;
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.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles.
* LDAP Group to role mapper specific to the "shield.authc.ldap" package
*/
public class LdapGroupToRoleMapper extends AbstractComponent {
public static final String DEFAULT_FILE_NAME = "role_mapping.yml";
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 boolean useUnmappedGroupsAsRoles;
private volatile ImmutableMap<LdapName, Set<String>> groupRoles;
private CopyOnWriteArrayList<RefreshListener> listeners;
public class LdapGroupToRoleMapper extends GroupToRoleMapper {
@Inject
public LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, null);
}
LdapGroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener 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, ResourceWatcherService.Frequency.HIGH);
listeners = new CopyOnWriteArrayList<>();
if (listener != null) {
listeners.add(listener);
}
}
synchronized void addListener(RefreshListener listener) {
listeners.add(listener);
}
public static Path resolveFile(Settings settings, Environment env) {
String location = settings.get(ROLE_MAPPING_FILE_SETTING);
if (location == null) {
return ShieldPlugin.resolveConfigFile(env, DEFAULT_FILE_NAME);
}
return Paths.get(location);
}
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);
}
}
/**
* 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));
}
}
if (logger.isDebugEnabled()) {
logger.debug("The roles [{}], are mapped from these LDAP groups [{}]", roles, groupDns);
}
return roles;
}
String getRelativeName(LdapName groupLdapName) {
return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue();
}
protected void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
}
}
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);
notifyRefresh();
}
}
}
static interface Listener {
final Listener NOOP = new Listener() {
@Override
public void onRefresh() {
}
};
void onRefresh();
super(settings, env, watcherService);
}
}

View File

@ -7,8 +7,8 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.support.AbstractShieldModule;
import static org.elasticsearch.common.inject.name.Names.named;
@ -19,9 +19,11 @@ import static org.elasticsearch.common.inject.name.Names.named;
public class LdapModule extends AbstractShieldModule.Node {
private final boolean enabled;
private final Settings ldapSettings;
public LdapModule(Settings settings) {
super(settings);
ldapSettings = settings.getComponentSettings(LdapModule.class);
enabled = enabled(settings);
}
@ -33,17 +35,6 @@ public class LdapModule extends AbstractShieldModule.Node {
requestStaticInjection(LdapSslSocketFactory.class);
bind(Realm.class).annotatedWith(named(LdapRealm.TYPE)).to(LdapRealm.class).asEagerSingleton();
bind(LdapGroupToRoleMapper.class).asEagerSingleton();
String mode = settings.getComponentSettings(LdapModule.class).get("mode", StandardLdapConnectionFactory.MODE_NAME);
if (StandardLdapConnectionFactory.MODE_NAME.equals(mode)) {
bind(LdapConnectionFactory.class).to(StandardLdapConnectionFactory.class);
} else if (ActiveDirectoryConnectionFactory.MODE_NAME.equals(mode)) {
bind(LdapConnectionFactory.class).to(ActiveDirectoryConnectionFactory.class);
} else {
throw new ShieldSettingsException("LDAP is enabled but mode [" + mode + "] does not match [" +
StandardLdapConnectionFactory.MODE_NAME + "] or [" + ActiveDirectoryConnectionFactory.MODE_NAME +"]");
}
} else {
bind(LdapRealm.class).toProvider(Providers.<LdapRealm>of(null));
}
@ -51,10 +42,10 @@ public class LdapModule extends AbstractShieldModule.Node {
public static boolean enabled(Settings settings) {
Settings authcSettings = settings.getAsSettings("shield.authc");
if (!authcSettings.names().contains("ldap")) {
if (!authcSettings.names().contains(LdapRealm.TYPE)) {
return false;
}
Settings ldapSettings = authcSettings.getAsSettings("ldap");
Settings ldapSettings = authcSettings.getAsSettings(LdapRealm.TYPE);
return ldapSettings.getAsBoolean("enabled", true);
}
}

View File

@ -8,73 +8,25 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.ShieldException;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.transport.TransportMessage;
import java.util.List;
import java.util.Set;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapRealm;
/**
* Authenticates username/password tokens against ldap, locates groups and maps them to roles.
*/
public class LdapRealm extends CachingUsernamePasswordRealm implements Realm<UsernamePasswordToken> {
public class LdapRealm extends AbstractLdapRealm {
public static final String TYPE = "ldap";
private final LdapConnectionFactory connectionFactory;
private final LdapGroupToRoleMapper roleMapper;
@Inject
public LdapRealm(Settings settings, LdapConnectionFactory ldap, LdapGroupToRoleMapper roleMapper, RestController restController) {
super(settings);
this.connectionFactory = ldap;
this.roleMapper = roleMapper;
roleMapper.addListener(new Listener());
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
public LdapRealm(Settings settings,
GenericLdapConnectionFactory ldap,
LdapGroupToRoleMapper roleMapper,
RestController restController) {
super(settings, ldap, roleMapper, restController);
}
@Override
public String type() {
return TYPE;
}
@Override
public UsernamePasswordToken token(TransportMessage<?> message) {
return UsernamePasswordToken.extractToken(message, null);
}
public boolean supports(AuthenticationToken token) {
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
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);
return new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));
} catch (ShieldException e){
if (logger.isDebugEnabled()) {
logger.debug("Authentication Failed for user [{}]", e, token.principal());
}
return null;
}
}
class Listener implements RefreshListener {
@Override
public void onRefresh() {
expireAll();
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.ShieldException;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.AuthenticationToken;
import org.elasticsearch.shield.authc.Realm;
import org.elasticsearch.shield.authc.support.CachingUsernamePasswordRealm;
import org.elasticsearch.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.transport.TransportMessage;
import java.util.List;
import java.util.Set;
/**
* Supporting class for JNDI-based Realms
*/
public abstract class AbstractLdapRealm extends CachingUsernamePasswordRealm implements Realm<UsernamePasswordToken> {
protected final LdapConnectionFactory connectionFactory;
protected final GroupToRoleMapper roleMapper;
public AbstractLdapRealm(Settings settings, LdapConnectionFactory ldap, GroupToRoleMapper roleMapper, RestController restController) {
super(settings);
this.connectionFactory = ldap;
this.roleMapper = roleMapper;
roleMapper.addListener(new Listener());
restController.registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
}
@Override
public UsernamePasswordToken token(TransportMessage<?> message) {
return UsernamePasswordToken.extractToken(message, null);
}
public boolean supports(AuthenticationToken token) {
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
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);
return new User.Simple(token.principal(), roles.toArray(new String[roles.size()]));
} catch (ShieldException e){
if (logger.isDebugEnabled()) {
logger.debug("Authentication Failed for user [{}]", e, token.principal());
}
return null;
}
}
class Listener implements RefreshListener {
@Override
public void onRefresh() {
expireAll();
}
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.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.shield.authc.support.RefreshListener;
import org.elasticsearch.shield.plugin.ShieldPlugin;
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.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* This class loads and monitors the file defining the mappings of LDAP Group DNs to internal ES Roles.
*/
public abstract class GroupToRoleMapper extends AbstractComponent {
public static final String DEFAULT_FILE_NAME = "role_mapping.yml";
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 boolean useUnmappedGroupsAsRoles;
private volatile ImmutableMap<LdapName, Set<String>> groupRoles;
private CopyOnWriteArrayList<RefreshListener> listeners;
@Inject
public GroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService) {
this(settings, env, watcherService, null);
}
GroupToRoleMapper(Settings settings, Environment env, ResourceWatcherService watcherService, RefreshListener 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, ResourceWatcherService.Frequency.HIGH);
listeners = new CopyOnWriteArrayList<>();
if (listener != null) {
listeners.add(listener);
}
}
public synchronized void addListener(RefreshListener listener) {
listeners.add(listener);
}
public static Path resolveFile(Settings settings, Environment env) {
String location = settings.get(ROLE_MAPPING_FILE_SETTING);
if (location == null) {
return ShieldPlugin.resolveConfigFile(env, DEFAULT_FILE_NAME);
}
return Paths.get(location);
}
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);
}
}
/**
* 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));
}
}
if (logger.isDebugEnabled()) {
logger.debug("The roles [{}], are mapped from these LDAP groups [{}]", roles, groupDns);
}
return roles;
}
String getRelativeName(LdapName groupLdapName) {
return (String) groupLdapName.getRdn(groupLdapName.size() - 1).getValue();
}
public void notifyRefresh() {
for (RefreshListener listener : listeners) {
listener.onRefresh();
}
}
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(GroupToRoleMapper.this.file.toFile())) {
groupRoles = parseFile(file.toPath(), logger);
notifyRefresh();
}
}
}
static interface Listener {
final Listener NOOP = new Listener() {
@Override
public void onRefresh() {
}
};
void onRefresh();
}
}

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import java.io.Closeable;
import java.util.List;

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.shield.authc.support.SecuredString;

View File

@ -3,8 +3,9 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.inject.Inject;
@ -46,7 +47,7 @@ public class LdapSslSocketFactory extends SocketFactory {
* This should only be invoked once to establish a static instance that will be used for each constructor.
*/
@Inject
public static void init(SSLService ssl) {
public static void init(@Nullable SSLService ssl) {
if (instance != null) {
logger.error("LdapSslSocketFactory already configured, this change could lead to threading issues");
}
@ -62,7 +63,7 @@ public class LdapSslSocketFactory extends SocketFactory {
* testing this is useful.
*/
@Deprecated
static void clear() {
public static void clear() {
logger.error("clear should only be called by tests");
instance = null;
}

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.ElasticsearchException;

View File

@ -11,7 +11,7 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.ldap.LdapModule;
import org.elasticsearch.shield.authc.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import javax.net.ssl.*;
import java.io.FileInputStream;

View File

@ -3,12 +3,16 @@
* 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;
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.authc.ldap.*;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.authc.support.ldap.LdapConnection;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.authc.support.ldap.LdapTest;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.hamcrest.Matchers;
@ -27,11 +31,11 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
public static final String AD_LDAP_URL = "ldaps://54.213.145.20:636";
public static final String PASSWORD = "NickFuryHeartsES";
public static final String AD_DOMAIN = "ad.test.elasticsearch.com";
public static String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
public static String SETTINGS_PREFIX = ActiveDirectoryRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
@BeforeClass
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("ldaptrust.jks").toURI()).getAbsoluteFile();
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
@ -50,8 +54,6 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
String userName = "ironman";
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
String userDN = ldap.getAuthenticatedUserDn();
List<String> groups = ldap.getGroups();
assertThat(groups, containsInAnyOrder(
containsString("Geniuses"),
@ -88,8 +90,6 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
String userName = "hulk";
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
String userDN = ldap.getAuthenticatedUserDn();
List<String> groups = ldap.getGroups();
assertThat(groups, containsInAnyOrder(
@ -97,7 +97,7 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
containsString("SHIELD"),
containsString("Geniuses"),
containsString("Philanthropists"),
containsString("Users"),
//containsString("Users"), Users group is in a different user context
containsString("Domain Users"),
containsString("Supers")
));
@ -109,7 +109,7 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
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(
GenericLdapConnectionFactory connectionFactory = new GenericLdapConnectionFactory(
LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Bruce Banner";

View File

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.inject.Guice;
import org.elasticsearch.common.inject.Injector;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
/**
*
*/
public class ActiveDirectoryModuleTests extends ElasticsearchTestCase {
private ThreadPool threadPool;
@Before
public void init() throws Exception {
threadPool = new ThreadPool("test");
}
@After
public void shutdown() {
threadPool.shutdownNow();
}
@Test
public void testEnabled() throws Exception {
assertThat(ActiveDirectoryModule.enabled(ImmutableSettings.EMPTY), is(false));
Settings settings = ImmutableSettings.builder()
.put("shield.authc", false)
.build();
assertThat(ActiveDirectoryModule.enabled(settings), is(false));
settings = ImmutableSettings.builder()
.put("shield.authc.active_directory.enabled", false)
.build();
assertThat(ActiveDirectoryModule.enabled(settings), is(false));
settings = ImmutableSettings.builder()
.put("shield.authc.active_directory.enabled", true)
.build();
assertThat(ActiveDirectoryModule.enabled(settings), is(true));
}
@Test
public void test() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("client.type", "node")
.put("shield.authc.active_directory.url", "ldap://example.com:389")
.put("shield.authc.active_directory.domain_name", "example.com")
.build();
Injector injector = Guice.createInjector(new TestModule(settings), new ActiveDirectoryModule(settings));
ActiveDirectoryRealm realm = injector.getInstance(ActiveDirectoryRealm.class);
assertThat(realm, notNullValue());
}
public class TestModule extends AbstractModule {
private final Settings settings;
public TestModule(Settings settings) {
this.settings = settings;
}
@Override
protected void configure() {
Environment env = new Environment(settings);
bind(Settings.class).toInstance(settings);
bind(Environment.class).toInstance(env);
bind(ThreadPool.class).toInstance(threadPool);
bind(ResourceWatcherService.class).asEagerSingleton();
bind(RestController.class).toInstance(mock(RestController.class));
bind(SSLService.class).toProvider(Providers.<SSLService>of(null));
}
}
}

View File

@ -1,169 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.io.File;
import java.net.URISyntaxException;
import java.util.Hashtable;
public class ADGroups {
public static void main (String[] args) throws URISyntaxException {
LdapSslSocketFactory.init(ImmutableSettings.builder()
.put("shield.authc.ldap.truststore", new File(LdapConnectionTests.class.getResource("ldaptrust.jks").toURI()))
.build());
Hashtable env = new Hashtable();
String adminName = "CN=Tony Stark,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
String adminPassword = "NickFuryHeartsES";
String ldapURL = "ldaps://ad.test.elasticsearch.com:636";
//set security credentials, note using simple cleartext authentication
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, adminName);
env.put(Context.SECURITY_CREDENTIALS, adminPassword);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put("java.naming.ldap.factory.socket", LdapSslSocketFactory.class.getName());
//connect to my domain controller
env.put(Context.PROVIDER_URL, ldapURL);
//specify attributes to be returned in binary format
env.put("java.naming.ldap.attributes.binary", "tokenGroups");
try {
//Create the initial directory context
LdapContext ctx = new InitialLdapContext(env,null);
//Create the search controls
SearchControls userSearchCtls = new SearchControls();
//Specify the search scope
userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
//specify the LDAP search filter to find the user in question
String userSearchFilter = "(objectClass=user)";
//paceholder for an LDAP filter that will store SIDs of the groups the user belongs to
StringBuffer groupsSearchFilter = new StringBuffer();
groupsSearchFilter.append("(|");
//Specify the Base for the search
String userSearchBase = "CN=Tony Stark,CN=Users,DC=ad,DC=test,DC=elasticsearch,DC=com";
//Specify the attributes to return
String userReturnedAtts[] = { "tokenGroups", "CN" };
userSearchCtls.setReturningAttributes(userReturnedAtts);
//Search for objects using the filter
NamingEnumeration userAnswer = ctx.search(userSearchBase, userSearchFilter, userSearchCtls);
//Loop through the search results
while (userAnswer.hasMoreElements()) {
SearchResult sr = (SearchResult)userAnswer.next();
Attributes attrs = sr.getAttributes();
if (attrs != null) {
try {
for (NamingEnumeration ae = attrs.getAll();ae.hasMore();) {
Attribute attr = (Attribute)ae.next();
for (NamingEnumeration e = attr.getAll();e.hasMore();) {
byte[] sid = (byte[])e.next();
groupsSearchFilter.append("(objectSid=" + binarySidToStringSid(sid) + ")");
}
groupsSearchFilter.append(")");
}
}
catch (NamingException e) {
System.err.println("Problem listing membership: " + e);
}
}
}
// Search for groups the user belongs to in order to get their names
//Create the search controls
SearchControls groupsSearchCtls = new SearchControls();
//Specify the search scope
groupsSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
//Specify the Base for the search
String groupsSearchBase = "DC=ad,DC=test,DC=elasticsearch,DC=com";
//Specify the attributes to return
String groupsReturnedAtts[]={};
groupsSearchCtls.setReturningAttributes(groupsReturnedAtts);
//Search for objects using the filter
NamingEnumeration groupsAnswer = ctx.search(groupsSearchBase, groupsSearchFilter.toString(), groupsSearchCtls);
//Loop through the search results
while (groupsAnswer.hasMoreElements()) {
SearchResult sr = (SearchResult)groupsAnswer.next();
System.out.println(sr.getNameInNamespace());
}
ctx.close();
}
catch (NamingException e) {
System.err.println("Problem searching directory: " + e);
}
}
public static final String binarySidToStringSid( byte[] SID ) {
String strSID = "";
//convert the SID into string format
long version;
long authority;
long count;
long rid;
strSID = "S";
version = SID[0];
strSID = strSID + "-" + Long.toString(version);
authority = SID[4];
for (int i = 0;i<4;i++) {
authority <<= 8;
authority += SID[4+i] & 0xFF;
}
strSID = strSID + "-" + Long.toString(authority);
count = SID[2];
count <<= 8;
count += SID[1] & 0xFF;
for (int j=0;j<count;j++) {
rid = SID[11 + (j*4)] & 0xFF;
for (int k=1;k<4;k++) {
rid <<= 8;
rid += SID[11-k + (j*4)] & 0xFF;
}
strSID = strSID + "-" + Long.toString(rid);
}
return strSID;
}
}

View File

@ -7,6 +7,7 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.ldap.LdapTest;
import org.junit.Test;
import java.util.List;
@ -26,7 +27,7 @@ public class LdapConnectionTests extends LdapTest {
"wrongname={0},ou=people,o=sevenSeas",
"cn={0},ou=people,o=sevenSeas", //this last one should work
};
StandardLdapConnectionFactory connectionFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory connectionFactory = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrls, userTemplates, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
@ -50,7 +51,7 @@ public class LdapConnectionTests extends LdapTest {
"wrongname={0},ou=people,o=sevenSeas",
"asdf={0},ou=people,o=sevenSeas", //none of these should work
};
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFac = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl, userTemplates, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
@ -65,7 +66,7 @@ public class LdapConnectionTests extends LdapTest {
String userTemplate = "cn={0},ou=people,o=sevenSeas";
boolean isSubTreeSearch = true;
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFac = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";
@ -82,7 +83,7 @@ public class LdapConnectionTests extends LdapTest {
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
String userTemplate = "cn={0},ou=people,o=sevenSeas";
boolean isSubTreeSearch = false;
StandardLdapConnectionFactory ldapFac = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFac = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
String user = "Horatio Hornblower";

View File

@ -8,6 +8,7 @@ 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.authc.support.ldap.GroupToRoleMapper;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
@ -52,12 +53,12 @@ public class LdapGroupToRoleMapperTest extends ElasticsearchTestCase {
@Test
public void testYaml() throws IOException {
File file = this.getResource("role_mapping.yml");
File file = this.getResource("../support/ldap/role_mapping.yml");
Settings settings = ImmutableSettings.settingsBuilder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.ROLE_MAPPING_FILE_SETTING, file.getCanonicalPath())
.build();
LdapGroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
GroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
new Environment(settings),
new ResourceWatcherService(settings, threadPool));
@ -70,10 +71,10 @@ public class LdapGroupToRoleMapperTest extends ElasticsearchTestCase {
@Test
public void testRelativeDN() {
Settings settings = ImmutableSettings.builder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.put("shield.authc.ldap." + GroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
LdapGroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
GroupToRoleMapper mapper = new LdapGroupToRoleMapper(settings,
new Environment(settings),
new ResourceWatcherService(settings, threadPool));

View File

@ -5,18 +5,43 @@
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.inject.Guice;
import org.elasticsearch.common.inject.Injector;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.Mockito.mock;
/**
*
*/
public class LdapModuleTests extends ElasticsearchTestCase {
private ThreadPool threadPool;
@Before
public void init() throws Exception {
threadPool = new ThreadPool("test");
}
@After
public void shutdown() {
threadPool.shutdownNow();
}
@Test
public void testEnabled() throws Exception {
assertThat(LdapModule.enabled(ImmutableSettings.EMPTY), is(false));
@ -33,4 +58,34 @@ public class LdapModuleTests extends ElasticsearchTestCase {
.build();
assertThat(LdapModule.enabled(settings), is(true));
}
@Test
public void test() throws Exception {
Settings settings = ImmutableSettings.builder()
.put("client.type", "node")
.put("shield.authc.ldap.url", "ldap://example.com:389")
.build();
Injector injector = Guice.createInjector(new TestModule(settings), new LdapModule(settings));
LdapRealm realm = injector.getInstance(LdapRealm.class);
assertThat(realm, notNullValue());
}
public class TestModule extends AbstractModule {
private final Settings settings;
public TestModule(Settings settings) {
this.settings = settings;
}
@Override
protected void configure() {
Environment env = new Environment(settings);
bind(Settings.class).toInstance(settings);
bind(Environment.class).toInstance(env);
bind(ThreadPool.class).toInstance(threadPool);
bind(ResourceWatcherService.class).asEagerSingleton();
bind(RestController.class).toInstance(mock(RestController.class));
bind(SSLService.class).toProvider(Providers.<SSLService>of(null));
}
}
}

View File

@ -12,6 +12,7 @@ import org.elasticsearch.shield.User;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.authc.support.ldap.LdapTest;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.After;
@ -48,7 +49,7 @@ public class LdapRealmTest extends LdapTest {
@Test
public void testRestHeaderRegistration() {
new LdapRealm(ImmutableSettings.EMPTY, mock(LdapConnectionFactory.class), mock(LdapGroupToRoleMapper.class), restController);
new LdapRealm(ImmutableSettings.EMPTY, mock(GenericLdapConnectionFactory.class), mock(LdapGroupToRoleMapper.class), restController);
verify(restController).registerRelevantHeaders(UsernamePasswordToken.BASIC_AUTH_HEADER);
}
@ -58,7 +59,8 @@ public class LdapRealmTest extends LdapTest {
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
Settings settings = buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch);
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(settings);
GenericLdapConnectionFactory ldapFactory = new GenericLdapConnectionFactory(settings);
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), restController);
User user = ldap.authenticate(new UsernamePasswordToken(VALID_USERNAME, SecuredStringTests.build(PASSWORD)));
@ -71,7 +73,7 @@ public class LdapRealmTest extends LdapTest {
String groupSearchBase = "ou=crews,ou=groups,o=sevenSeas";
boolean isSubTreeSearch = false;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFactory = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
LdapRealm ldap = new LdapRealm(buildNonCachingSettings(), ldapFactory, buildGroupAsRoleMapper(resourceWatcherService), restController);
@ -86,7 +88,7 @@ public class LdapRealmTest extends LdapTest {
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFactory = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
ldapFactory = spy(ldapFactory);
@ -103,7 +105,7 @@ public class LdapRealmTest extends LdapTest {
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFactory = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch) );
LdapGroupToRoleMapper roleMapper = buildGroupAsRoleMapper(resourceWatcherService);
@ -129,7 +131,7 @@ public class LdapRealmTest extends LdapTest {
String groupSearchBase = "o=sevenSeas";
boolean isSubTreeSearch = true;
String userTemplate = VALID_USER_TEMPLATE;
StandardLdapConnectionFactory ldapFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory ldapFactory = new GenericLdapConnectionFactory(
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
ldapFactory = spy(ldapFactory);

View File

@ -8,6 +8,7 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.AfterClass;
@ -28,7 +29,7 @@ public class OpenLdapTests extends ElasticsearchTestCase {
@BeforeClass
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("ldaptrust.jks").toURI()).getAbsoluteFile();
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
@ -47,7 +48,7 @@ public class OpenLdapTests extends ElasticsearchTestCase {
String groupSearchBase = "ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
String userTemplate = "uid={0},ou=people,dc=oldap,dc=test,dc=elasticsearch,dc=com";
boolean isSubTreeSearch = true;
StandardLdapConnectionFactory connectionFactory = new StandardLdapConnectionFactory(
GenericLdapConnectionFactory connectionFactory = new GenericLdapConnectionFactory(
LdapConnectionTests.buildLdapSettings(OPEN_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
String[] users = new String[]{"blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor"};

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import com.carrotsearch.randomizedtesting.SysGlobals;
import org.apache.directory.api.ldap.model.entry.Entry;

View File

@ -3,12 +3,13 @@
* 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;
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.authc.ldap.LdapConnectionTests;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.hamcrest.Matchers;
import org.junit.AfterClass;
@ -25,7 +26,7 @@ public class LdapSslSocketFactoryTests extends ElasticsearchTestCase {
@BeforeClass
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("ldaptrust.jks").toURI()).getAbsoluteFile();
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
@ -45,7 +46,7 @@ public class LdapSslSocketFactoryTests extends ElasticsearchTestCase {
LdapSslSocketFactory.configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(LdapSslSocketFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET),
Matchers.<Serializable>equalTo("org.elasticsearch.shield.authc.ldap.LdapSslSocketFactory"));
Matchers.<Serializable>equalTo("org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory"));
}
@Test
@ -55,7 +56,7 @@ public class LdapSslSocketFactoryTests extends ElasticsearchTestCase {
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder();
LdapSslSocketFactory.configureJndiSSL(urls, builder);
ImmutableMap<String, Serializable> settings = builder.build();
assertThat(settings.get(LdapSslSocketFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), Matchers.<Serializable>equalTo("org.elasticsearch.shield.authc.ldap.LdapSslSocketFactory"));
assertThat(settings.get(LdapSslSocketFactory.JAVA_NAMING_LDAP_FACTORY_SOCKET), Matchers.<Serializable>equalTo("org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory"));
}
@Test

View File

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.ldap;
package org.elasticsearch.shield.authc.support.ldap;
import com.carrotsearch.randomizedtesting.LifecycleScope;
import com.carrotsearch.randomizedtesting.ThreadFilter;
@ -11,6 +11,9 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.shield.authc.ldap.GenericLdapConnectionFactory;
import org.elasticsearch.shield.authc.ldap.LdapGroupToRoleMapper;
import org.elasticsearch.shield.authc.ldap.LdapRealm;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.AfterClass;
@ -41,16 +44,16 @@ public abstract class LdapTest extends ElasticsearchTestCase {
return ldap.getUrl();
}
static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
public static Settings buildLdapSettings(String ldapUrl, String userTemplate, String groupSearchBase, boolean isSubTreeSearch) {
return buildLdapSettings( new String[]{ldapUrl}, new String[]{userTemplate}, groupSearchBase, isSubTreeSearch );
}
static Settings buildLdapSettings(String[] ldapUrl, String[] userTemplate, String groupSearchBase, boolean 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();
.putArray(SETTINGS_PREFIX + GenericLdapConnectionFactory.URLS_SETTING, ldapUrl)
.putArray(SETTINGS_PREFIX + GenericLdapConnectionFactory.USER_DN_TEMPLATES_SETTING, userTemplate)
.put(SETTINGS_PREFIX + GenericLdapConnectionFactory.GROUP_SEARCH_BASEDN_SETTING, groupSearchBase)
.put(SETTINGS_PREFIX + GenericLdapConnectionFactory.GROUP_SEARCH_SUBTREE_SETTING, isSubTreeSearch).build();
}
protected Settings buildNonCachingSettings() {
@ -66,7 +69,7 @@ public abstract class LdapTest extends ElasticsearchTestCase {
protected LdapGroupToRoleMapper buildGroupAsRoleMapper(ResourceWatcherService resourceWatcherService) {
Settings settings = ImmutableSettings.builder()
.put("shield.authc.ldap." + LdapGroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.put("shield.authc.ldap." + GroupToRoleMapper.USE_UNMAPPED_GROUPS_AS_ROLES_SETTING, true)
.build();
return new LdapGroupToRoleMapper(settings, new Environment(settings), resourceWatcherService);