LDAP: Changed AD group lookup to include nested groups
This expands the ActiveDirectoryConnectionFactory to lookup nested groups, in a non standard way. Fixes https://github.com/elasticsearch/elasticsearch-shield/issues/286 Original commit: elastic/x-pack-elasticsearch@01781a8305
This commit is contained in:
parent
34a69cd1cf
commit
852529bf53
|
@ -0,0 +1,166 @@
|
||||||
|
/*
|
||||||
|
* 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.collect.ImmutableList;
|
||||||
|
import org.elasticsearch.common.logging.ESLogger;
|
||||||
|
import org.elasticsearch.common.logging.ESLoggerFactory;
|
||||||
|
|
||||||
|
import javax.naming.NamingEnumeration;
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.naming.directory.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class ActiveDirectoryConnection implements LdapConnection {
|
||||||
|
private static final ESLogger logger = ESLoggerFactory.getLogger(GenericLdapConnection.class.getName());
|
||||||
|
private final String bindDn;
|
||||||
|
protected final DirContext ldapContext;
|
||||||
|
|
||||||
|
private final String groupSearchDN;
|
||||||
|
protected final String groupAttribute = "memberOf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object is intended to be constructed by the LdapConnectionFactory
|
||||||
|
*/
|
||||||
|
ActiveDirectoryConnection(DirContext ctx, String boundName, String groupSearchDN) {
|
||||||
|
this.ldapContext = ctx;
|
||||||
|
this.bindDn = boundName;
|
||||||
|
this.groupSearchDN = groupSearchDN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
ldapContext.close();
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new LdapException("Could not close the LDAP connection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getGroups() {
|
||||||
|
|
||||||
|
String groupsSearchFilter = buildGroupQuery();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
ImmutableList.Builder<String> groups = ImmutableList.<String>builder();
|
||||||
|
try {
|
||||||
|
//Search for objects using the filter
|
||||||
|
NamingEnumeration groupsAnswer = ldapContext.search(groupsSearchBase, groupsSearchFilter.toString(), groupsSearchCtls);
|
||||||
|
|
||||||
|
//Loop through the search results
|
||||||
|
while (groupsAnswer.hasMoreElements()) {
|
||||||
|
SearchResult sr = (SearchResult) groupsAnswer.next();
|
||||||
|
groups.add(sr.getNameInNamespace());
|
||||||
|
}
|
||||||
|
} catch (NamingException ne) {
|
||||||
|
throw new LdapException("Exception occurred fetching AD groups", bindDn, ne);
|
||||||
|
}
|
||||||
|
return groups.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildGroupQuery() {
|
||||||
|
StringBuffer groupsSearchFilter = new StringBuffer("(|");
|
||||||
|
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);
|
||||||
|
NamingEnumeration userAnswer = ldapContext.search(getAuthenticatedUserDn(), userSearchFilter, userSearchCtls);
|
||||||
|
|
||||||
|
//Loop through the search results
|
||||||
|
while (userAnswer.hasMoreElements()) {
|
||||||
|
|
||||||
|
SearchResult sr = (SearchResult)userAnswer.next();
|
||||||
|
Attributes attrs = sr.getAttributes();
|
||||||
|
|
||||||
|
if (attrs != null) {
|
||||||
|
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 ne) {
|
||||||
|
throw new LdapException("Exception occurred fetching AD groups", bindDn, ne);
|
||||||
|
}
|
||||||
|
return groupsSearchFilter.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthenticatedUserDn() {
|
||||||
|
return bindDn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = "";
|
||||||
|
|
||||||
|
//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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
||||||
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
|
ImmutableMap.Builder<String, Serializable> builder = ImmutableMap.<String, Serializable>builder()
|
||||||
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
|
.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
|
||||||
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
|
.put(Context.PROVIDER_URL, Strings.arrayToCommaDelimitedString(ldapUrls))
|
||||||
|
.put("java.naming.ldap.attributes.binary", "tokenGroups")
|
||||||
.put(Context.REFERRAL, "follow");
|
.put(Context.REFERRAL, "follow");
|
||||||
|
|
||||||
LdapSslSocketFactory.configureJndiSSL(ldapUrls, builder);
|
LdapSslSocketFactory.configureJndiSSL(ldapUrls, builder);
|
||||||
|
@ -66,7 +67,7 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
||||||
* @return An authenticated
|
* @return An authenticated
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LdapConnection bind(String userName, SecuredString password) {
|
public ActiveDirectoryConnection bind(String userName, SecuredString password) {
|
||||||
String userPrincipal = userName + "@" + this.domainName;
|
String userPrincipal = userName + "@" + this.domainName;
|
||||||
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
||||||
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
||||||
|
@ -77,7 +78,7 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
||||||
DirContext ctx = new InitialDirContext(ldapEnv);
|
DirContext ctx = new InitialDirContext(ldapEnv);
|
||||||
SearchControls searchCtls = new SearchControls();
|
SearchControls searchCtls = new SearchControls();
|
||||||
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
||||||
searchCtls.setReturningAttributes( new String[0] );
|
searchCtls.setReturningAttributes( new String[] {} );
|
||||||
|
|
||||||
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
|
String searchFilter = "(&(objectClass=user)(userPrincipalName={0}))";
|
||||||
NamingEnumeration<SearchResult> results = ctx.search(userSearchDN, searchFilter, new Object[]{ userPrincipal }, searchCtls);
|
NamingEnumeration<SearchResult> results = ctx.search(userSearchDN, searchFilter, new Object[]{ userPrincipal }, searchCtls);
|
||||||
|
@ -87,8 +88,7 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
||||||
String name = entry.getNameInNamespace();
|
String name = entry.getNameInNamespace();
|
||||||
|
|
||||||
if (!results.hasMore()) {
|
if (!results.hasMore()) {
|
||||||
//isFindGroupsByAttribute=true, group subtree search=false, groupSubtreeDN=null
|
return new ActiveDirectoryConnection(ctx, name, userSearchDN);
|
||||||
return new LdapConnection(ctx, name, true, false, null);
|
|
||||||
}
|
}
|
||||||
throw new LdapException("Search for user [" + userName + "] by principle name yielded multiple results");
|
throw new LdapException("Search for user [" + userName + "] by principle name yielded multiple results");
|
||||||
}
|
}
|
||||||
|
@ -107,4 +107,5 @@ public class ActiveDirectoryConnectionFactory extends AbstractComponent implemen
|
||||||
String buildDnFromDomain(String domain) {
|
String buildDnFromDomain(String domain) {
|
||||||
return "DC=" + domain.replace(".", ",DC=");
|
return "DC=" + domain.replace(".", ",DC=");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
* 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.logging.ESLogger;
|
||||||
|
import org.elasticsearch.common.logging.ESLoggerFactory;
|
||||||
|
|
||||||
|
import javax.naming.NamingEnumeration;
|
||||||
|
import javax.naming.NamingException;
|
||||||
|
import javax.naming.directory.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming
|
||||||
|
* instances of this connection will be produced by the LdapConnectionFactory.bind() methods.
|
||||||
|
*
|
||||||
|
* A standard looking usage pattern could look like this:
|
||||||
|
<pre>
|
||||||
|
try (LdapConnection session = ldapFac.bindXXX(...);
|
||||||
|
...do stuff with the session
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
*/
|
||||||
|
public class GenericLdapConnection implements LdapConnection {
|
||||||
|
|
||||||
|
private static final ESLogger logger = ESLoggerFactory.getLogger(GenericLdapConnection.class.getName());
|
||||||
|
private final String bindDn;
|
||||||
|
protected final DirContext ldapContext;
|
||||||
|
|
||||||
|
private final String groupSearchDN;
|
||||||
|
private final boolean isGroupSubTreeSearch;
|
||||||
|
private final boolean isFindGroupsByAttribute;
|
||||||
|
protected final String groupAttribute = "memberOf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object is intended to be constructed by the LdapConnectionFactory
|
||||||
|
*/
|
||||||
|
GenericLdapConnection(DirContext ctx, String boundName, boolean isFindGroupsByAttribute, boolean isGroupSubTreeSearch, String groupSearchDN) {
|
||||||
|
this.ldapContext = ctx;
|
||||||
|
this.bindDn = boundName;
|
||||||
|
this.isGroupSubTreeSearch = isGroupSubTreeSearch;
|
||||||
|
this.groupSearchDN = groupSearchDN;
|
||||||
|
this.isFindGroupsByAttribute = isFindGroupsByAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LDAP connections should be closed to clean up resources. However, the jndi contexts have the finalize
|
||||||
|
* implemented properly so that it will clean up on garbage collection.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close(){
|
||||||
|
try {
|
||||||
|
ldapContext.close();
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new SecurityException("Could not close the LDAP connection", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the groups that the user is a member of
|
||||||
|
* @return List of group membership
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> getGroups(){
|
||||||
|
List<String> groups = isFindGroupsByAttribute ? getGroupsFromUserAttrs(bindDn) : getGroupsFromSearch(bindDn);
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Found these 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 getGroups as the template method).
|
||||||
|
* @param userDn user fully distinguished name to fetch group membership for
|
||||||
|
* @return fully distinguished names of the roles
|
||||||
|
*/
|
||||||
|
List<String> getGroupsFromSearch(String userDn){
|
||||||
|
List<String> groups = new LinkedList<>();
|
||||||
|
SearchControls search = new SearchControls();
|
||||||
|
search.setReturningAttributes( new String[0] );
|
||||||
|
search.setSearchScope( this.isGroupSubTreeSearch ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE);
|
||||||
|
|
||||||
|
//This could be made could be made configurable but it should cover all cases
|
||||||
|
String filter = "(&" +
|
||||||
|
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group)) " +
|
||||||
|
"(|(uniqueMember={0})(member={0})))";
|
||||||
|
|
||||||
|
try {
|
||||||
|
NamingEnumeration<SearchResult> results = ldapContext.search(
|
||||||
|
groupSearchDN, filter, new Object[]{userDn}, search);
|
||||||
|
while (results.hasMoreElements()){
|
||||||
|
groups.add(results.next().getNameInNamespace());
|
||||||
|
}
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new LdapException("Could not search for an LDAP group for user [" + userDn + "]", e);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the groups from the user attributes (if supported). This method could later be abstracted out
|
||||||
|
* into a strategy class
|
||||||
|
* @param userDn User fully distinguished name to fetch group membership from
|
||||||
|
* @return list of groups the user is a member of.
|
||||||
|
*/
|
||||||
|
List<String> getGroupsFromUserAttrs(String userDn) {
|
||||||
|
List<String> groupDns = new LinkedList<>();
|
||||||
|
try {
|
||||||
|
Attributes results = ldapContext.getAttributes(userDn, new String[]{groupAttribute});
|
||||||
|
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
|
||||||
|
Attribute attr = (Attribute)ae.next();
|
||||||
|
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
|
||||||
|
Object val = attrEnum.next();
|
||||||
|
if (val instanceof String) {
|
||||||
|
String stringVal = (String) val;
|
||||||
|
groupDns.add(stringVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new LdapException("Could not look up group attributes for user [" + userDn + "]", e);
|
||||||
|
}
|
||||||
|
return groupDns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches common user attributes from the user. Its a good way to ensure a connection works.
|
||||||
|
*/
|
||||||
|
public Map<String,String[]> getUserAttrs(String userDn) {
|
||||||
|
Map <String, String[]>userAttrs = new HashMap<>();
|
||||||
|
try {
|
||||||
|
Attributes results = ldapContext.getAttributes(userDn, new String[]{"uid", "memberOf", "isMemberOf"});
|
||||||
|
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
|
||||||
|
Attribute attr = (Attribute)ae.next();
|
||||||
|
LinkedList<String> attrList = new LinkedList<>();
|
||||||
|
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
|
||||||
|
Object val = attrEnum.next();
|
||||||
|
if (val instanceof String) {
|
||||||
|
String stringVal = (String) val;
|
||||||
|
attrList.add(stringVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String[] attrArray = attrList.toArray(new String[attrList.size()]);
|
||||||
|
userAttrs.put(attr.getID(), attrArray);
|
||||||
|
}
|
||||||
|
} catch (NamingException e) {
|
||||||
|
throw new LdapException("Could not look up attributes for user [" + userDn + "]", e);
|
||||||
|
}
|
||||||
|
return userAttrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthenticatedUserDn() {
|
||||||
|
return bindDn;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,157 +5,16 @@
|
||||||
*/
|
*/
|
||||||
package org.elasticsearch.shield.authc.ldap;
|
package org.elasticsearch.shield.authc.ldap;
|
||||||
|
|
||||||
import org.elasticsearch.common.logging.ESLogger;
|
|
||||||
import org.elasticsearch.common.logging.ESLoggerFactory;
|
|
||||||
|
|
||||||
import javax.naming.NamingEnumeration;
|
|
||||||
import javax.naming.NamingException;
|
|
||||||
import javax.naming.directory.*;
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encapsulates jndi/ldap functionality into one authenticated connection. The constructor is package scoped, assuming
|
* Represents a LDAP connection with an authenticated/bound user that needs closing.
|
||||||
* instances of this connection will be produced by the LdapConnectionFactory.bind() methods.
|
|
||||||
*
|
|
||||||
* A standard looking usage pattern could look like this:
|
|
||||||
<pre>
|
|
||||||
try (LdapConnection session = ldapFac.bindXXX(...);
|
|
||||||
...do stuff with the session
|
|
||||||
}
|
|
||||||
</pre>
|
|
||||||
*/
|
*/
|
||||||
public class LdapConnection implements Closeable {
|
public interface LdapConnection extends Closeable {
|
||||||
|
void close();
|
||||||
|
|
||||||
private static final ESLogger logger = ESLoggerFactory.getLogger(LdapConnection.class.getName());
|
List<String> getGroups();
|
||||||
private final String bindDn;
|
|
||||||
private final DirContext ldapContext;
|
|
||||||
|
|
||||||
private final String groupSearchDN;
|
String getAuthenticatedUserDn();
|
||||||
private final boolean isGroupSubTreeSearch;
|
|
||||||
private final boolean isFindGroupsByAttribute;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This object is intended to be constructed by the LdapConnectionFactory
|
|
||||||
*/
|
|
||||||
LdapConnection(DirContext ctx, String boundName, boolean isFindGroupsByAttribute, boolean isGroupSubTreeSearch, String groupSearchDN) {
|
|
||||||
this.ldapContext = ctx;
|
|
||||||
this.bindDn = boundName;
|
|
||||||
this.isGroupSubTreeSearch = isGroupSubTreeSearch;
|
|
||||||
this.groupSearchDN = groupSearchDN;
|
|
||||||
this.isFindGroupsByAttribute = isFindGroupsByAttribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LDAP connections should be closed to clean up resources. However, the jndi contexts have the finalize
|
|
||||||
* implemented properly so that it will clean up on garbage collection.
|
|
||||||
*/
|
|
||||||
public void close(){
|
|
||||||
try {
|
|
||||||
ldapContext.close();
|
|
||||||
} catch (NamingException e) {
|
|
||||||
throw new SecurityException("Could not close the LDAP connection", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the groups that the user is a member of
|
|
||||||
* @return List of group membership
|
|
||||||
*/
|
|
||||||
public List<String> getGroups(){
|
|
||||||
List<String> groups = isFindGroupsByAttribute ? getGroupsFromUserAttrs(bindDn) : getGroupsFromSearch(bindDn);
|
|
||||||
if (logger.isDebugEnabled()) {
|
|
||||||
logger.debug("Found these 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 getGroups as the template method).
|
|
||||||
* @param userDn user fully distinguished name to fetch group membership for
|
|
||||||
* @return fully distinguished names of the roles
|
|
||||||
*/
|
|
||||||
List<String> getGroupsFromSearch(String userDn){
|
|
||||||
List<String> groups = new LinkedList<>();
|
|
||||||
SearchControls search = new SearchControls();
|
|
||||||
search.setReturningAttributes( new String[0] );
|
|
||||||
search.setSearchScope( this.isGroupSubTreeSearch ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE);
|
|
||||||
|
|
||||||
//This could be made could be made configurable but it should cover all cases
|
|
||||||
String filter = "(&" +
|
|
||||||
"(|(objectclass=groupOfNames)(objectclass=groupOfUniqueNames)(objectclass=group)) " +
|
|
||||||
"(|(uniqueMember={0})(member={0})))";
|
|
||||||
|
|
||||||
try {
|
|
||||||
NamingEnumeration<SearchResult> results = ldapContext.search(
|
|
||||||
groupSearchDN, filter, new Object[]{userDn}, search);
|
|
||||||
while (results.hasMoreElements()){
|
|
||||||
groups.add(results.next().getNameInNamespace());
|
|
||||||
}
|
|
||||||
} catch (NamingException e) {
|
|
||||||
throw new LdapException("Could not search for an LDAP group for user [" + userDn + "]", e);
|
|
||||||
}
|
|
||||||
return groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the groups from the user attributes (if supported). This method could later be abstracted out
|
|
||||||
* into a strategy class
|
|
||||||
* @param userDn User fully distinguished name to fetch group membership from
|
|
||||||
* @return list of groups the user is a member of.
|
|
||||||
*/
|
|
||||||
List<String> getGroupsFromUserAttrs(String userDn) {
|
|
||||||
List<String> groupDns = new LinkedList<>();
|
|
||||||
try {
|
|
||||||
Attributes results = ldapContext.getAttributes(userDn, new String[]{"memberOf", "isMemberOf"});
|
|
||||||
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
|
|
||||||
Attribute attr = (Attribute)ae.next();
|
|
||||||
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
|
|
||||||
Object val = attrEnum.next();
|
|
||||||
if (val instanceof String) {
|
|
||||||
String stringVal = (String) val;
|
|
||||||
groupDns.add(stringVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (NamingException e) {
|
|
||||||
throw new LdapException("Could not look up group attributes for user [" + userDn + "]", e);
|
|
||||||
}
|
|
||||||
return groupDns;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches common user attributes from the user. Its a good way to ensure a connection works.
|
|
||||||
*/
|
|
||||||
public Map<String,String[]> getUserAttrs(String userDn) {
|
|
||||||
Map <String, String[]>userAttrs = new HashMap<>();
|
|
||||||
try {
|
|
||||||
Attributes results = ldapContext.getAttributes(userDn, new String[]{"uid", "memberOf", "isMemberOf"});
|
|
||||||
for(NamingEnumeration ae = results.getAll(); ae.hasMore();) {
|
|
||||||
Attribute attr = (Attribute)ae.next();
|
|
||||||
LinkedList<String> attrList = new LinkedList<>();
|
|
||||||
for (NamingEnumeration attrEnum = attr.getAll(); attrEnum.hasMore();) {
|
|
||||||
Object val = attrEnum.next();
|
|
||||||
if (val instanceof String) {
|
|
||||||
String stringVal = (String) val;
|
|
||||||
attrList.add(stringVal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String[] attrArray = attrList.toArray(new String[attrList.size()]);
|
|
||||||
userAttrs.put(attr.getID(), attrArray);
|
|
||||||
}
|
|
||||||
} catch (NamingException e) {
|
|
||||||
throw new LdapException("Could not look up attributes for user [" + userDn + "]", e);
|
|
||||||
}
|
|
||||||
return userAttrs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthenticatedUserDn() {
|
|
||||||
return bindDn;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
|
||||||
* @return authenticated exception
|
* @return authenticated exception
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public LdapConnection bind(String username, SecuredString password) {
|
public GenericLdapConnection bind(String username, SecuredString password) {
|
||||||
//SASL, MD5, etc. all options here stink, we really need to go over ssl + simple authentication
|
//SASL, MD5, etc. all options here stink, we really need to go over ssl + simple authentication
|
||||||
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
Hashtable<String, Serializable> ldapEnv = new Hashtable<>(this.sharedLdapEnv);
|
||||||
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
|
||||||
|
@ -86,7 +86,7 @@ public class StandardLdapConnectionFactory extends AbstractComponent implements
|
||||||
DirContext ctx = new InitialDirContext(ldapEnv);
|
DirContext ctx = new InitialDirContext(ldapEnv);
|
||||||
|
|
||||||
//return the first good connection
|
//return the first good connection
|
||||||
return new LdapConnection(ctx, dn, findGroupsByAttribute, groupSubTreeSearch, groupSearchDN);
|
return new GenericLdapConnection(ctx, dn, findGroupsByAttribute, groupSubTreeSearch, groupSearchDN);
|
||||||
|
|
||||||
} catch (NamingException e) {
|
} catch (NamingException e) {
|
||||||
logger.warn("Failed ldap authentication with user template [{}], dn [{}]", e, template, dn );
|
logger.warn("Failed ldap authentication with user template [{}], dn [{}]", e, template, dn );
|
||||||
|
|
|
@ -52,14 +52,18 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
||||||
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
|
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
|
||||||
String userDN = ldap.getAuthenticatedUserDn();
|
String userDN = ldap.getAuthenticatedUserDn();
|
||||||
|
|
||||||
List<String> groups = ldap.getGroupsFromUserAttrs(userDN);
|
List<String> groups = ldap.getGroups();
|
||||||
assertThat(groups, containsInAnyOrder(
|
assertThat(groups, containsInAnyOrder(
|
||||||
containsString("Geniuses"),
|
containsString("Geniuses"),
|
||||||
containsString("Billionaire"),
|
containsString("Billionaire"),
|
||||||
containsString("Playboy"),
|
containsString("Playboy"),
|
||||||
containsString("Philanthropists"),
|
containsString("Philanthropists"),
|
||||||
containsString("Avengers"),
|
containsString("Avengers"),
|
||||||
containsString("SHIELD")));
|
containsString("SHIELD"),
|
||||||
|
containsString("Users"),
|
||||||
|
containsString("Domain Users"),
|
||||||
|
containsString("Supers")
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,13 +90,17 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
||||||
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
|
try (LdapConnection ldap = connectionFactory.bind(userName, SecuredStringTests.build(PASSWORD))) {
|
||||||
String userDN = ldap.getAuthenticatedUserDn();
|
String userDN = ldap.getAuthenticatedUserDn();
|
||||||
|
|
||||||
List<String> groups = ldap.getGroupsFromUserAttrs(userDN);
|
List<String> groups = ldap.getGroups();
|
||||||
|
|
||||||
assertThat(groups, containsInAnyOrder(
|
assertThat(groups, containsInAnyOrder(
|
||||||
containsString("Avengers"),
|
containsString("Avengers"),
|
||||||
containsString("SHIELD"),
|
containsString("SHIELD"),
|
||||||
containsString("Geniuses"),
|
containsString("Geniuses"),
|
||||||
containsString("Philanthropists")));
|
containsString("Philanthropists"),
|
||||||
|
containsString("Users"),
|
||||||
|
containsString("Domain Users"),
|
||||||
|
containsString("Supers")
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +113,7 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
|
||||||
LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
|
LdapTest.buildLdapSettings(AD_LDAP_URL, userTemplate, groupSearchBase, isSubTreeSearch));
|
||||||
|
|
||||||
String user = "Bruce Banner";
|
String user = "Bruce Banner";
|
||||||
try (LdapConnection ldap = connectionFactory.bind(user, SecuredStringTests.build(PASSWORD))) {
|
try (GenericLdapConnection ldap = connectionFactory.bind(user, SecuredStringTests.build(PASSWORD))) {
|
||||||
List<String> groups = ldap.getGroupsFromUserAttrs(ldap.getAuthenticatedUserDn());
|
List<String> groups = ldap.getGroupsFromUserAttrs(ldap.getAuthenticatedUserDn());
|
||||||
List<String> groups2 = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
List<String> groups2 = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ public class LdapConnectionTests extends LdapTest {
|
||||||
String user = "Horatio Hornblower";
|
String user = "Horatio Hornblower";
|
||||||
SecuredString userPass = SecuredStringTests.build("pass");
|
SecuredString userPass = SecuredStringTests.build("pass");
|
||||||
|
|
||||||
try (LdapConnection ldap = connectionFactory.bind(user, userPass)) {
|
try (GenericLdapConnection ldap = connectionFactory.bind(user, userPass)) {
|
||||||
Map<String, String[]> attrs = ldap.getUserAttrs(ldap.getAuthenticatedUserDn());
|
Map<String, String[]> attrs = ldap.getUserAttrs(ldap.getAuthenticatedUserDn());
|
||||||
|
|
||||||
assertThat(attrs, hasKey("uid"));
|
assertThat(attrs, hasKey("uid"));
|
||||||
|
@ -55,7 +55,7 @@ public class LdapConnectionTests extends LdapTest {
|
||||||
|
|
||||||
String user = "Horatio Hornblower";
|
String user = "Horatio Hornblower";
|
||||||
SecuredString userPass = SecuredStringTests.build("pass");
|
SecuredString userPass = SecuredStringTests.build("pass");
|
||||||
try (LdapConnection ldapConnection = ldapFac.bind(user, userPass)) {
|
try (GenericLdapConnection ldapConnection = ldapFac.bind(user, userPass)) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ public class LdapConnectionTests extends LdapTest {
|
||||||
String user = "Horatio Hornblower";
|
String user = "Horatio Hornblower";
|
||||||
SecuredString userPass = SecuredStringTests.build("pass");
|
SecuredString userPass = SecuredStringTests.build("pass");
|
||||||
|
|
||||||
try (LdapConnection ldap = ldapFac.bind(user, userPass)) {
|
try (GenericLdapConnection ldap = ldapFac.bind(user, userPass)) {
|
||||||
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||||
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
|
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ public class LdapConnectionTests extends LdapTest {
|
||||||
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
buildLdapSettings(ldapUrl(), userTemplate, groupSearchBase, isSubTreeSearch));
|
||||||
|
|
||||||
String user = "Horatio Hornblower";
|
String user = "Horatio Hornblower";
|
||||||
try (LdapConnection ldap = ldapFac.bind(user, SecuredStringTests.build("pass"))) {
|
try (GenericLdapConnection ldap = ldapFac.bind(user, SecuredStringTests.build("pass"))) {
|
||||||
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
List<String> groups = ldap.getGroupsFromSearch(ldap.getAuthenticatedUserDn());
|
||||||
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
|
assertThat(groups, contains("cn=HMS Lydia,ou=crews,ou=groups,o=sevenSeas"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ public class OpenLdapTests extends ElasticsearchTestCase {
|
||||||
|
|
||||||
String[] users = new String[]{"blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor"};
|
String[] users = new String[]{"blackwidow", "cap", "hawkeye", "hulk", "ironman", "thor"};
|
||||||
for(String user: users) {
|
for(String user: users) {
|
||||||
LdapConnection ldap = connectionFactory.bind(user, SecuredStringTests.build(PASSWORD));
|
GenericLdapConnection ldap = connectionFactory.bind(user, SecuredStringTests.build(PASSWORD));
|
||||||
assertThat(ldap.getGroups(), hasItem(containsString("Avengers")));
|
assertThat(ldap.getGroups(), hasItem(containsString("Avengers")));
|
||||||
ldap.close();
|
ldap.close();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue