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:
parent
79d4b1e208
commit
da5299e4c5
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue