LDAP refactoring

- Introduced a strategy for group search. This is applied on the `AbstractLdapConnection` level.
- The `LdapConnection` and `ActiveDirectoryConnection` are now clean of logic
- The `AbstractLdapConnection` holds a timeout
- Introduced `SearchScope` for better settings support.
- fixed a bug in `LdapConnectionFactory:74`... `settings.getAsArray` will never return `null`

Original commit: elastic/x-pack-elasticsearch@1f4a43d037
This commit is contained in:
uboness 2015-01-17 01:39:16 +01:00 committed by c-a-m
parent 79d4b1e208
commit da5299e4c5
9 changed files with 388 additions and 297 deletions

View File

@ -5,167 +5,22 @@
*/
package org.elasticsearch.shield.authc.active_directory;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.shield.authc.ldap.LdapException;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.List;
import javax.naming.directory.DirContext;
/**
* An Ldap Connection customized for active directory.
*/
public class ActiveDirectoryConnection extends AbstractLdapConnection {
private final String groupSearchDN;
private final int timeoutMilliseconds;
/**
* This object is intended to be constructed by the LdapConnectionFactory
*/
ActiveDirectoryConnection(ESLogger logger, DirContext ctx, String boundName, String groupSearchDN, int timeoutMilliseconds) {
super(logger, ctx, boundName);
this.groupSearchDN = groupSearchDN;
this.timeoutMilliseconds = timeoutMilliseconds;
ActiveDirectoryConnection(ESLogger logger, DirContext ctx, String boundName, String groupSearchDN, TimeValue timeout) {
super(logger, ctx, boundName, new ActiveDirectoryGroupsResolver(logger, groupSearchDN), timeout);
}
/**
* LDAP connections should be closed to clean up resources. However, the jndi contexts have the finalize
* implemented properly so that it will clean up on garbage collection.
*/
@Override
public void close() {
try {
jndiContext.close();
} catch (NamingException e) {
throw new ActiveDirectoryException("could not close the LDAP connection", e);
}
}
@Override
public List<String> groups() {
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
SearchControls groupsSearchCtls = new SearchControls();
//Specify the search scope
groupsSearchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
groupsSearchCtls.setReturningAttributes(Strings.EMPTY_ARRAY); //we only need the entry DN
groupsSearchCtls.setTimeLimit(timeoutMilliseconds);
ImmutableList.Builder<String> groups = ImmutableList.builder();
try (ClosableNamingEnumeration<SearchResult> groupsAnswer = new ClosableNamingEnumeration<>(
jndiContext.search(groupSearchDN, groupsSearchFilter, groupsSearchCtls))) {
//Loop through the search results
while (groupsAnswer.hasMoreElements()) {
SearchResult sr = groupsAnswer.next();
groups.add(sr.getNameInNamespace());
}
} catch (NamingException | LdapException ne) {
throw new ActiveDirectoryException("failed to fetch AD groups", bindDn, ne);
}
List<String> groupList = groups.build();
if (logger.isDebugEnabled()) {
logger.debug("found these groups [{}] for userDN [{}]", groupList, this.bindDn);
}
return groupList;
}
private String buildGroupQuery() {
StringBuilder groupsSearchFilter = new StringBuilder("(|");
try {
SearchControls userSearchCtls = new SearchControls();
userSearchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
//specify the LDAP search filter to find the user in question
String userSearchFilter = "(objectClass=user)";
String userReturnedAtts[] = { "tokenGroups" };
userSearchCtls.setReturningAttributes(userReturnedAtts);
try (ClosableNamingEnumeration<SearchResult> userAnswer = new ClosableNamingEnumeration<>(
jndiContext.search(authenticatedUserDn(), userSearchFilter, userSearchCtls))) {
//Loop through the search results
while (userAnswer.hasMoreElements()) {
SearchResult sr = userAnswer.next();
Attributes attrs = sr.getAttributes();
if (attrs != null) {
try (ClosableNamingEnumeration<? extends Attribute> ae = new ClosableNamingEnumeration<>(attrs.getAll())) {
while (ae.hasMore() ) {
Attribute attr = ae.next();
for (NamingEnumeration e = attr.getAll(); e.hasMore(); ) {
byte[] sid = (byte[]) e.next();
groupsSearchFilter.append("(objectSid=");
groupsSearchFilter.append(binarySidToStringSid(sid));
groupsSearchFilter.append(")");
}
groupsSearchFilter.append(")");
}
}
}
}
}
} catch (NamingException | LdapException ne) {
throw new ActiveDirectoryException("failed to fetch AD groups", bindDn, ne);
}
return groupsSearchFilter.toString();
}
@Override
public String authenticatedUserDn() {
return bindDn;
}
/**
* To better understand what the sid is and how its string representation looks like, see
* http://blogs.msdn.com/b/alextch/archive/2007/06/18/sample-java-application-that-retrieves-group-membership-of-an-active-directory-user-account.aspx
*
* @param SID byte encoded security ID
*/
static public String binarySidToStringSid(byte[] SID) {
String strSID;
//convert the SID into string format
long version;
long authority;
long count;
long rid;
strSID = "S";
version = SID[0];
strSID = strSID + "-" + Long.toString(version);
authority = SID[4];
for (int i = 0; i < 4; i++) {
authority <<= 8;
authority += SID[4 + i] & 0xFF;
}
strSID = strSID + "-" + Long.toString(authority);
count = SID[2];
count <<= 8;
count += SID[1] & 0xFF;
for (int j = 0; j < count; j++) {
rid = SID[11 + (j * 4)] & 0xFF;
for (int k = 1; k < 4; k++) {
rid <<= 8;
rid += SID[11 - k + (j * 4)] & 0xFF;
}
strSID = strSID + "-" + Long.toString(rid);
}
return strSID;
}
}

View File

@ -9,12 +9,14 @@ import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.ldap.LdapException;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import javax.naming.Context;
import javax.naming.NamingException;
@ -36,14 +38,14 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDi
public static final String AD_DOMAIN_NAME_SETTING = "domain_name";
public static final String AD_USER_SEARCH_BASEDN_SETTING = "user_search.base_dn";
public static final String AD_USER_SEARCH_FILTER_SETTING = "user_search.filter";
public static final String AD_USER_SEARCH_SUBTREE_SETTING = "user_search.subtree";
public static final String AD_USER_SEARCH_SCOPE_SETTING = "user_search.scope";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String userSearchDN;
private final String domainName;
private final String userSearchFilter;
private final int timeoutMilliseconds;
private final Boolean userSearchSubtree;
private final SearchScope userSearchScope;
private final TimeValue timeout;
@Inject
public ActiveDirectoryConnectionFactory(RealmConfig config) {
@ -55,8 +57,8 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDi
}
userSearchDN = settings.get(AD_USER_SEARCH_BASEDN_SETTING, buildDnFromDomain(domainName));
userSearchFilter = settings.get(AD_USER_SEARCH_FILTER_SETTING, "(&(objectClass=user)(|(sAMAccountName={0})(userPrincipalName={0}@" + domainName + ")))");
userSearchSubtree = settings.getAsBoolean(AD_USER_SEARCH_SUBTREE_SETTING, true);
timeoutMilliseconds = (int) settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT).millis();
userSearchScope = SearchScope.resolve(settings.get(AD_USER_SEARCH_SCOPE_SETTING), SearchScope.SUB_TREE);
timeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
String[] ldapUrls = settings.getAsArray(URLS_SETTING, new String[] { "ldaps://" + domainName + ":636" });
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
@ -90,9 +92,9 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDi
try {
ctx = new InitialDirContext(ldapEnv);
SearchControls searchCtls = new SearchControls();
searchCtls.setSearchScope(userSearchSubtree ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE);
searchCtls.setSearchScope(userSearchScope.scope());
searchCtls.setReturningAttributes(Strings.EMPTY_ARRAY);
searchCtls.setTimeLimit(timeoutMilliseconds);
searchCtls.setTimeLimit((int) timeout.millis());
try (ClosableNamingEnumeration<SearchResult> results = new ClosableNamingEnumeration<>(
ctx.search(userSearchDN, userSearchFilter, new Object[] { userName }, searchCtls))) {
@ -101,16 +103,16 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDi
String name = entry.getNameInNamespace();
if (!results.hasMore()) {
return new ActiveDirectoryConnection(connectionLogger, ctx, name, userSearchDN, timeoutMilliseconds);
return new ActiveDirectoryConnection(connectionLogger, ctx, name, userSearchDN, timeout);
}
ctx.close();
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded multiple results");
} else {
ctx.close();
throw new ActiveDirectoryException("search for user [" + userName + "] by principle name yielded no results");
}
}
} catch (NamingException | LdapException e) {
throw new ActiveDirectoryException("unable to authenticate user [" + userName + "] to active directory domain [" + domainName + "]", e);
} finally {
if (ctx != null) {
try {
ctx.close();
@ -118,7 +120,6 @@ public class ActiveDirectoryConnectionFactory extends ConnectionFactory<ActiveDi
logger.trace("an unexpected error occurred closing an LDAP context", ne);
}
}
throw new ActiveDirectoryException("unable to authenticate user [" + userName + "] to active directory domain [" + domainName + "]", e);
}
}

View File

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

View File

@ -5,17 +5,11 @@
*/
package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.LinkedList;
import java.util.List;
import javax.naming.directory.DirContext;
/**
* Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming
@ -30,109 +24,10 @@ import java.util.List;
*/
public class LdapConnection extends AbstractLdapConnection {
private final int timeoutMilliseconds;
private final boolean isGroupSubTreeSearch;
private final boolean isFindGroupsByAttribute;
private final String groupSearchDN;
private final String groupAttribute = "memberOf";
private final String userAttributeForGroupMembership;
private final String groupSearchFilter;
/**
* This object is intended to be constructed by the LdapConnectionFactory
*/
LdapConnection(ESLogger logger, DirContext ctx, String bindDN, int timeoutMilliseconds, boolean isFindGroupsByAttribute, boolean isGroupSubTreeSearch,
@Nullable String groupSearchFilter, @Nullable String groupSearchDN, @Nullable String userAttributeForGroupMembership) {
super(logger, ctx, bindDN);
this.isGroupSubTreeSearch = isGroupSubTreeSearch;
this.groupSearchFilter = groupSearchFilter;
this.groupSearchDN = groupSearchDN;
this.isFindGroupsByAttribute = isFindGroupsByAttribute;
this.userAttributeForGroupMembership = userAttributeForGroupMembership;
this.timeoutMilliseconds = timeoutMilliseconds;
}
/**
* Fetches the groups that the user is a member of
*
* @return List of group membership
*/
@Override
public List<String> groups() {
List<String> groups = isFindGroupsByAttribute ? getGroupsFromUserAttrs() : getGroupsFromSearch();
if (logger.isDebugEnabled()) {
logger.debug("found groups [{}] for userDN [{}]", groups, this.bindDn);
}
return groups;
}
/**
* 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 groups as the template method).
* @return fully distinguished names of the roles
*/
public List<String> getGroupsFromSearch() {
String userIdentifier = userAttributeForGroupMembership == null ? bindDn : readUserAttribute(userAttributeForGroupMembership);
if (logger.isTraceEnabled()) {
logger.trace("user identifier for group lookup is [{}]", userIdentifier);
}
List<String> groups = new LinkedList<>();
SearchControls search = new SearchControls();
search.setReturningAttributes(Strings.EMPTY_ARRAY);
search.setSearchScope(this.isGroupSubTreeSearch ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE);
search.setTimeLimit(timeoutMilliseconds);
try (ClosableNamingEnumeration<SearchResult> results = new ClosableNamingEnumeration<>(
jndiContext.search(groupSearchDN, groupSearchFilter, new Object[] {userIdentifier}, search))) {
while (results.hasMoreElements()) {
groups.add(results.next().getNameInNamespace());
}
} catch (NamingException | LdapException e ) {
throw new LdapException("could not search for an LDAP group", bindDn, e);
}
return groups;
}
/**
* Fetches the groups from the user attributes (if supported). This method could later be abstracted out
* into a strategy class
*
* @return list of groups the user is a member of.
*/
public List<String> getGroupsFromUserAttrs() {
List<String> groupDns = new LinkedList<>();
try {
Attributes results = jndiContext.getAttributes(bindDn, new String[] { groupAttribute });
try (ClosableNamingEnumeration<? extends Attribute> ae = new ClosableNamingEnumeration<>(results.getAll())) {
while (ae.hasMore()) {
Attribute attr = ae.next();
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore(); ) {
Object val = attrEnum.next();
if (val instanceof String) {
String stringVal = (String) val;
groupDns.add(stringVal);
}
}
}
}
} catch (NamingException | LdapException e) {
throw new LdapException("could not look up group attributes for user", bindDn, e);
}
return groupDns;
}
String readUserAttribute(String userAttribute) {
try {
Attributes results = jndiContext.getAttributes(bindDn, new String[]{userAttribute});
Attribute attribute = results.get(userAttribute);
if (results.size() == 0) {
throw new LdapException("No results returned for attribute [" + userAttribute + "]", bindDn);
}
return (String) attribute.get();
} catch (NamingException e) {
throw new LdapException("Could not look attribute [" + userAttribute + "]", bindDn, e);
} catch (ClassCastException e) {
throw new LdapException("Returned ldap attribute [" + userAttribute + "] is not of type String", bindDn, e);
}
LdapConnection(ESLogger logger, DirContext ctx, String bindDN, AbstractLdapConnection.GroupsResolver groupsResolver, TimeValue timeout) {
super(logger, ctx, bindDN, groupsResolver, timeout);
}
}

View File

@ -9,9 +9,11 @@ import org.elasticsearch.common.Strings;
import org.elasticsearch.common.collect.ImmutableMap;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.RealmConfig;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ConnectionFactory;
import javax.naming.Context;
@ -32,25 +34,13 @@ import java.util.Hashtable;
public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
public static final String USER_DN_TEMPLATES_SETTING = "user_dn_templates";
public static final String GROUP_SEARCH_SUBTREE_SETTING = "group_search.subtree";
public static final String GROUP_SEARCH_BASEDN_SETTING = "group_search.base_dn";
public static final String GROUP_SEARCH_FILTER_SETTING = "group_search.filter";
public static final String GROUP_SEARCH_USER_ATTRIBUTE_SETTING = "group_search.user_attribute";
private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" +
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group))" +
"(|(uniqueMember={0})(member={0})))";
private final ImmutableMap<String, Serializable> sharedLdapEnv;
private final String[] userDnTemplates;
protected final String groupSearchDN;
protected final boolean groupSubTreeSearch;
protected final boolean findGroupsByAttribute;
private final int timeoutMilliseconds;
private final String groupFilter;
private final String groupSearchUserAttribute;
private final AbstractLdapConnection.GroupsResolver groupResolver;
private final TimeValue timeout;
@Inject()
@Inject
public LdapConnectionFactory(RealmConfig config) {
super(LdapConnection.class, config);
Settings settings = config.settings();
@ -59,11 +49,10 @@ public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
throw new ShieldSettingsException("missing required LDAP setting [" + USER_DN_TEMPLATES_SETTING + "]");
}
String[] ldapUrls = settings.getAsArray(URLS_SETTING);
if (ldapUrls == null) {
if (ldapUrls == null || ldapUrls.length == 0) {
throw new ShieldSettingsException("missing required LDAP setting [" + URLS_SETTING + "]");
}
timeoutMilliseconds = (int) settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT).millis();
timeout = settings.getAsTime(TIMEOUT_LDAP_SETTING, TIMEOUT_DEFAULT);
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
@ -75,12 +64,8 @@ public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
configureJndiSSL(ldapUrls, builder);
sharedLdapEnv = builder.build();
groupSearchDN = settings.get(GROUP_SEARCH_BASEDN_SETTING);
findGroupsByAttribute = groupSearchDN == null;
groupSubTreeSearch = settings.getAsBoolean(GROUP_SEARCH_SUBTREE_SETTING, true);
groupFilter = settings.get(GROUP_SEARCH_FILTER_SETTING, GROUP_SEARCH_DEFAULT_FILTER);
groupSearchUserAttribute = settings.get(GROUP_SEARCH_FILTER_SETTING) == null ?
null : settings.get(GROUP_SEARCH_USER_ATTRIBUTE_SETTING); //if filter isn't set we don't want to change from using the DN
groupResolver = groupResolver(settings);
}
/**
@ -104,8 +89,7 @@ public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
DirContext ctx = new InitialDirContext(ldapEnv);
//return the first good connection
return new LdapConnection(connectionLogger, ctx, dn, timeoutMilliseconds, findGroupsByAttribute, groupSubTreeSearch,
groupFilter, groupSearchDN, groupSearchUserAttribute);
return new LdapConnection(connectionLogger, ctx, dn, groupResolver, timeout);
} catch (NamingException e) {
logger.warn("failed LDAP authentication with user template [{}] and DN [{}]", e, template, dn);
@ -126,4 +110,12 @@ public class LdapConnectionFactory extends ConnectionFactory<LdapConnection> {
String escapedUsername = Rdn.escapeValue(username);
return MessageFormat.format(template, escapedUsername);
}
static AbstractLdapConnection.GroupsResolver groupResolver(Settings settings) {
Settings searchSettings = settings.getAsSettings("group_search");
if (!searchSettings.names().isEmpty()) {
return new SearchGroupsResolver(searchSettings);
}
return new UserAttributeGroupsResolver(settings);
}
}

View File

@ -0,0 +1,83 @@
/*
* 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.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import org.elasticsearch.shield.authc.support.ldap.SearchScope;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
class SearchGroupsResolver implements AbstractLdapConnection.GroupsResolver {
private static final String GROUP_SEARCH_DEFAULT_FILTER = "(&" +
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group))" +
"(|(uniqueMember={0})(member={0})))";
private final String baseDn;
private final String filter;
private final String userAttribute;
private final SearchScope scope;
public SearchGroupsResolver(Settings settings) {
baseDn = settings.get("base_dn");
filter = settings.get("filter", GROUP_SEARCH_DEFAULT_FILTER);
userAttribute = filter == null ? null : settings.get("user_attribute");
scope = SearchScope.resolve(settings.get("scope"), SearchScope.SUB_TREE);
}
public SearchGroupsResolver(String baseDn, String filter, String userAttribute, SearchScope scope) {
this.baseDn = baseDn;
this.filter = filter;
this.userAttribute = userAttribute;
this.scope = scope;
}
@Override
public List<String> resolve(DirContext ctx, String userDn, TimeValue timeout) {
List<String> groups = new LinkedList<>();
String userId = userAttribute != null ? readUserAttribute(ctx, userDn, userDn) : userDn;
SearchControls search = new SearchControls();
search.setReturningAttributes(Strings.EMPTY_ARRAY);
search.setSearchScope(scope.scope());
search.setTimeLimit((int) timeout.millis());
try (ClosableNamingEnumeration<SearchResult> results = new ClosableNamingEnumeration<>(
ctx.search(baseDn, filter, new Object[] { userId }, search))) {
while (results.hasMoreElements()) {
groups.add(results.next().getNameInNamespace());
}
} catch (NamingException | LdapException e ) {
throw new LdapException("could not search for an LDAP group", userDn, e);
}
return groups;
}
String readUserAttribute(DirContext ctx, String userDn, String userAttribute) {
try {
Attributes results = ctx.getAttributes(userDn, new String[]{userAttribute});
Attribute attribute = results.get(userAttribute);
if (results.size() == 0) {
throw new LdapException("No results returned for attribute [" + userAttribute + "]", userDn);
}
return (String) attribute.get();
} catch (NamingException e) {
throw new LdapException("Could not look attribute [" + userAttribute + "]", userDn, e);
} catch (ClassCastException e) {
throw new LdapException("Returned ldap attribute [" + userAttribute + "] is not of type String", userDn, e);
}
}
}

View File

@ -0,0 +1,58 @@
/*
* 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.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.shield.authc.support.ldap.AbstractLdapConnection;
import org.elasticsearch.shield.authc.support.ldap.ClosableNamingEnumeration;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import java.util.LinkedList;
import java.util.List;
/**
*
*/
class UserAttributeGroupsResolver implements AbstractLdapConnection.GroupsResolver {
private final String attribute;
public UserAttributeGroupsResolver(Settings settings) {
this(settings.get("user_group_attribute", "memberOf"));
}
public UserAttributeGroupsResolver(String attribute) {
this.attribute = attribute;
}
@Override
public List<String> resolve(DirContext ctx, String userDn, TimeValue timeout) {
List<String> groupDns = new LinkedList<>();
try {
Attributes results = ctx.getAttributes(userDn, new String[] { attribute });
try (ClosableNamingEnumeration<? extends Attribute> ae = new ClosableNamingEnumeration<>(results.getAll())) {
while (ae.hasMore()) {
Attribute attr = ae.next();
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore(); ) {
Object val = attrEnum.next();
if (val instanceof String) {
String stringVal = (String) val;
groupDns.add(stringVal);
}
}
}
}
} catch (NamingException | LdapException e) {
throw new LdapException("could not look up group attributes for user", userDn, e);
}
return groupDns;
}
}

View File

@ -6,6 +6,7 @@
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.unit.TimeValue;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
@ -20,6 +21,8 @@ public abstract class AbstractLdapConnection implements Closeable {
protected final ESLogger logger;
protected final DirContext jndiContext;
protected final String bindDn;
protected final GroupsResolver groupsResolver;
protected final TimeValue timeout;
/**
* This object is intended to be constructed by the LdapConnectionFactory
@ -29,10 +32,12 @@ public abstract class AbstractLdapConnection implements Closeable {
* outside of and be reused across all connections. We can't keep a static logger in this class
* since we want the logger to be contextual (i.e. aware of the settings and its enviorment).
*/
public AbstractLdapConnection(ESLogger logger, DirContext ctx, String boundName) {
public AbstractLdapConnection(ESLogger logger, DirContext ctx, String boundName, GroupsResolver groupsResolver, TimeValue timeout) {
this.logger = logger;
this.jndiContext = ctx;
this.bindDn = boundName;
this.groupsResolver = groupsResolver;
this.timeout = timeout;
}
/**
@ -58,5 +63,13 @@ public abstract class AbstractLdapConnection implements Closeable {
/**
* @return List of fully distinguished group names
*/
public abstract List<String> groups();
public List<String> groups() {
return groupsResolver.resolve(jndiContext, bindDn, timeout);
}
public static interface GroupsResolver {
List<String> resolve(DirContext ctx, String userDn, TimeValue timeout);
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.authc.support.ldap;
import org.elasticsearch.shield.authc.ldap.LdapException;
import javax.naming.directory.SearchControls;
/**
*
*/
public enum SearchScope {
BASE(SearchControls.OBJECT_SCOPE),
ONE_LEVEL(SearchControls.ONELEVEL_SCOPE),
SUB_TREE(SearchControls.SUBTREE_SCOPE);
private final int scope;
SearchScope(int scope) {
this.scope = scope;
}
public int scope() {
return scope;
}
public static SearchScope resolve(String scope, SearchScope defaultScope) {
if (scope == null) {
return defaultScope;
}
switch (scope.toLowerCase()) {
case "base":
case "object": return BASE;
case "one_level" : return ONE_LEVEL;
case "sub_tree" : return SUB_TREE;
default:
throw new LdapException("Unknown search scope [" + scope + "]");
}
}
}