HADOOP-18388. Allow dynamic groupSearchFilter in LdapGroupsMapping. (#4798)

* HADOOP-18388. Allow dynamic groupSearchFilter in LdapGroupsMapping.
This commit is contained in:
Ayush Saxena 2022-09-07 04:08:51 +05:30 committed by GitHub
parent c947c326e8
commit cc41ad63f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 117 additions and 3 deletions

View File

@ -30,6 +30,7 @@
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyStore; import java.security.KeyStore;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator; import java.util.Iterator;
@ -252,6 +253,10 @@ public class LdapGroupsMapping
public static final String POSIX_GID_ATTR_KEY = LDAP_CONFIG_PREFIX + ".posix.attr.gid.name"; public static final String POSIX_GID_ATTR_KEY = LDAP_CONFIG_PREFIX + ".posix.attr.gid.name";
public static final String POSIX_GID_ATTR_DEFAULT = "gidNumber"; public static final String POSIX_GID_ATTR_DEFAULT = "gidNumber";
public static final String GROUP_SEARCH_FILTER_PATTERN =
LDAP_CONFIG_PREFIX + ".group.search.filter.pattern";
public static final String GROUP_SEARCH_FILTER_PATTERN_DEFAULT = "";
/* /*
* Posix attributes * Posix attributes
*/ */
@ -337,6 +342,7 @@ public class LdapGroupsMapping
private int numAttempts; private int numAttempts;
private volatile int numAttemptsBeforeFailover; private volatile int numAttemptsBeforeFailover;
private volatile String ldapCtxFactoryClassName; private volatile String ldapCtxFactoryClassName;
private volatile String[] groupSearchFilterParams;
/** /**
* Returns list of groups for a user. * Returns list of groups for a user.
@ -437,8 +443,14 @@ Set<String> lookupGroup(SearchResult result, DirContext c,
Set<String> groupDNs = new HashSet<>(); Set<String> groupDNs = new HashSet<>();
NamingEnumeration<SearchResult> groupResults; NamingEnumeration<SearchResult> groupResults;
// perform the second LDAP query
if (isPosix) { String[] resolved = resolveCustomGroupFilterArgs(result);
// If custom group filter argument is supplied, use that!!!
if (resolved != null) {
groupResults =
c.search(groupbaseDN, groupSearchFilter, resolved, SEARCH_CONTROLS);
} else if (isPosix) {
// perform the second LDAP query
groupResults = lookupPosixGroup(result, c); groupResults = lookupPosixGroup(result, c);
} else { } else {
String userDn = result.getNameInNamespace(); String userDn = result.getNameInNamespace();
@ -462,6 +474,25 @@ Set<String> lookupGroup(SearchResult result, DirContext c,
return groups; return groups;
} }
private String[] resolveCustomGroupFilterArgs(SearchResult result)
throws NamingException {
if (groupSearchFilterParams != null) {
String[] filterElems = new String[groupSearchFilterParams.length];
for (int i = 0; i < groupSearchFilterParams.length; i++) {
// Specific handling for userDN.
if (groupSearchFilterParams[i].equalsIgnoreCase("userDN")) {
filterElems[i] = result.getNameInNamespace();
} else {
filterElems[i] =
result.getAttributes().get(groupSearchFilterParams[i]).get()
.toString();
}
}
return filterElems;
}
return null;
}
/** /**
* Perform LDAP queries to get group names of a user. * Perform LDAP queries to get group names of a user.
* *
@ -781,6 +812,12 @@ public synchronized void setConf(Configuration conf) {
conf.get(POSIX_UID_ATTR_KEY, POSIX_UID_ATTR_DEFAULT); conf.get(POSIX_UID_ATTR_KEY, POSIX_UID_ATTR_DEFAULT);
posixGidAttr = posixGidAttr =
conf.get(POSIX_GID_ATTR_KEY, POSIX_GID_ATTR_DEFAULT); conf.get(POSIX_GID_ATTR_KEY, POSIX_GID_ATTR_DEFAULT);
String groupSearchFilterParamCSV = conf.get(GROUP_SEARCH_FILTER_PATTERN,
GROUP_SEARCH_FILTER_PATTERN_DEFAULT);
if(groupSearchFilterParamCSV!=null && !groupSearchFilterParamCSV.isEmpty()) {
LOG.debug("Using custom group search filters: {}", groupSearchFilterParamCSV);
groupSearchFilterParams = groupSearchFilterParamCSV.split(",");
}
int dirSearchTimeout = conf.getInt(DIRECTORY_SEARCH_TIMEOUT, int dirSearchTimeout = conf.getInt(DIRECTORY_SEARCH_TIMEOUT,
DIRECTORY_SEARCH_TIMEOUT_DEFAULT); DIRECTORY_SEARCH_TIMEOUT_DEFAULT);
@ -795,7 +832,16 @@ public synchronized void setConf(Configuration conf) {
returningAttributes = new String[] { returningAttributes = new String[] {
groupNameAttr, posixUidAttr, posixGidAttr}; groupNameAttr, posixUidAttr, posixGidAttr};
} }
SEARCH_CONTROLS.setReturningAttributes(returningAttributes);
// If custom group filter is being used, fetch attributes in the filter
// as well.
ArrayList<String> customAttributes = new ArrayList<>();
if (groupSearchFilterParams != null) {
customAttributes.addAll(Arrays.asList(groupSearchFilterParams));
}
customAttributes.addAll(Arrays.asList(returningAttributes));
SEARCH_CONTROLS
.setReturningAttributes(customAttributes.toArray(new String[0]));
// LDAP_CTX_FACTORY_CLASS_DEFAULT is not open to unnamed modules // LDAP_CTX_FACTORY_CLASS_DEFAULT is not open to unnamed modules
// in Java 11+, so the default value is set to null to avoid // in Java 11+, so the default value is set to null to avoid

View File

@ -585,6 +585,18 @@
</description> </description>
</property> </property>
<property>
<name>hadoop.security.group.mapping.ldap.group.search.filter.pattern</name>
<value></value>
<description>
Comma separated values that needs to be substituted in the group search
filter during group lookup. The values are substituted in the order they
appear in the list, the first value will replace {0} the second {1} and
so on.
</description>
</property>
<property> <property>
<name>hadoop.security.group.mapping.providers</name> <name>hadoop.security.group.mapping.providers</name>
<value></value> <value></value>

View File

@ -85,6 +85,14 @@ This is the limit for each ldap query. If `hadoop.security.group.mapping.ldap.s
`hadoop.security.group.mapping.ldap.base` configures how far to walk up the groups hierarchy when resolving groups. `hadoop.security.group.mapping.ldap.base` configures how far to walk up the groups hierarchy when resolving groups.
By default, with a limit of 0, in order to be considered a member of a group, the user must be an explicit member in LDAP. Otherwise, it will traverse the group hierarchy `hadoop.security.group.mapping.ldap.search.group.hierarchy.levels` levels up. By default, with a limit of 0, in order to be considered a member of a group, the user must be an explicit member in LDAP. Otherwise, it will traverse the group hierarchy `hadoop.security.group.mapping.ldap.search.group.hierarchy.levels` levels up.
It is possible to have custom group search filters with different arguments using
the configuration `hadoop.security.group.mapping.ldap.group.search.filter.pattern`, we can configure comma separated values here and the values configured will be fetched from the LDAP attributes and will be replaced in the group
search filter in the order they appear here, say if the first entry here is uid, so uid will be fetched from the attributes and the value fetched
will be used in place of {0} in the group search filter, similarly the second value configured will replace {1} and so on.
Note: If `hadoop.security.group.mapping.ldap.group.search.filter.pattern` is configured, the group search will always be done assuming this group
search filter pattern irrespective of any other parameters.
### Bind user(s) ### ### Bind user(s) ###
If the LDAP server does not support anonymous binds, If the LDAP server does not support anonymous binds,
set the distinguished name of the user to bind in `hadoop.security.group.mapping.ldap.bind.user`. set the distinguished name of the user to bind in `hadoop.security.group.mapping.ldap.bind.user`.

View File

@ -18,6 +18,7 @@
package org.apache.hadoop.security; package org.apache.hadoop.security;
import static org.apache.hadoop.security.LdapGroupsMapping.CONNECTION_TIMEOUT; import static org.apache.hadoop.security.LdapGroupsMapping.CONNECTION_TIMEOUT;
import static org.apache.hadoop.security.LdapGroupsMapping.GROUP_SEARCH_FILTER_PATTERN;
import static org.apache.hadoop.security.LdapGroupsMapping.LDAP_NUM_ATTEMPTS_KEY; import static org.apache.hadoop.security.LdapGroupsMapping.LDAP_NUM_ATTEMPTS_KEY;
import static org.apache.hadoop.security.LdapGroupsMapping.READ_TIMEOUT; import static org.apache.hadoop.security.LdapGroupsMapping.READ_TIMEOUT;
import static org.apache.hadoop.test.GenericTestUtils.assertExceptionContains; import static org.apache.hadoop.test.GenericTestUtils.assertExceptionContains;
@ -27,6 +28,8 @@
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -44,6 +47,8 @@
import javax.naming.CommunicationException; import javax.naming.CommunicationException;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
@ -120,6 +125,49 @@ public void testGetGroupsWithDefaultBaseDN() throws Exception {
doTestGetGroupsWithBaseDN(conf, baseDN.trim(), baseDN.trim()); doTestGetGroupsWithBaseDN(conf, baseDN.trim(), baseDN.trim());
} }
@Test
public void testGetGroupsWithDynamicGroupFilter() throws Exception {
// Set basic mock stuff.
Configuration conf = getBaseConf(TEST_LDAP_URL);
String baseDN = "dc=xxx,dc=com";
conf.set(LdapGroupsMapping.BASE_DN_KEY, baseDN);
Attributes attributes = getAttributes();
// Set the groupFilter conf to take the csv.
conf.set(GROUP_SEARCH_FILTER_PATTERN, "userDN,userName");
// Set the value for userName attribute that is to be used as part of the
// group filter at argument 1.
final String userName = "some_user";
Attribute userNameAttr = mock(Attribute.class);
when(userNameAttr.get()).thenReturn(userName);
when(attributes.get(eq("userName"))).thenReturn(userNameAttr);
// Set the dynamic group search filter.
final String groupSearchFilter =
"(|(memberUid={0})(uname={1}))" + "(objectClass=group)";
conf.set(LdapGroupsMapping.GROUP_SEARCH_FILTER_KEY, groupSearchFilter);
final LdapGroupsMapping groupsMapping = getGroupsMapping();
groupsMapping.setConf(conf);
// The group search filter should be resolved and should be passed as the
// below.
String groupFilter = "(|(memberUid={0})(uname={1}))(objectClass=group)";
String[] resolvedFilterArgs =
new String[] {"CN=some_user,DC=test,DC=com", "some_user"};
// Return groups only if the resolved filter is passed.
when(getContext()
.search(anyString(), eq(groupFilter), eq(resolvedFilterArgs),
any(SearchControls.class)))
.thenReturn(getUserNames(), getGroupNames());
// Check the group filter got resolved and get the desired values.
List<String> groups = groupsMapping.getGroups(userName);
Assert.assertEquals(Arrays.asList(getTestGroups()), groups);
}
/** /**
* Helper method to do the LDAP getGroups operation using given user base DN * Helper method to do the LDAP getGroups operation using given user base DN
* and group base DN. * and group base DN.