HADOOP-14465. LdapGroupsMapping - support user and group search base. Contributed by Shwetha G S and Mingliang Liu

This commit is contained in:
Mingliang Liu 2017-06-09 14:55:07 -07:00
parent 5578af8603
commit a2121cb0d9
4 changed files with 118 additions and 8 deletions

View File

@ -130,6 +130,19 @@ public class LdapGroupsMapping
public static final String BASE_DN_KEY = LDAP_CONFIG_PREFIX + ".base";
public static final String BASE_DN_DEFAULT = "";
/*
* Base DN used in user search.
*/
public static final String USER_BASE_DN_KEY =
LDAP_CONFIG_PREFIX + ".userbase";
/*
* Base DN used in group search.
*/
public static final String GROUP_BASE_DN_KEY =
LDAP_CONFIG_PREFIX + ".groupbase";
/*
* Any additional filters to apply when searching for users
*/
@ -200,7 +213,7 @@ public class LdapGroupsMapping
private static final Log LOG = LogFactory.getLog(LdapGroupsMapping.class);
private static final SearchControls SEARCH_CONTROLS = new SearchControls();
static final SearchControls SEARCH_CONTROLS = new SearchControls();
static {
SEARCH_CONTROLS.setSearchScope(SearchControls.SUBTREE_SCOPE);
}
@ -214,7 +227,8 @@ public class LdapGroupsMapping
private String keystorePass;
private String bindUser;
private String bindPassword;
private String baseDN;
private String userbaseDN;
private String groupbaseDN;
private String groupSearchFilter;
private String userSearchFilter;
private String memberOfAttr;
@ -315,7 +329,7 @@ private NamingEnumeration<SearchResult> lookupPosixGroup(SearchResult result,
uidNumber = uidAttribute.get().toString();
}
if (uidNumber != null && gidNumber != null) {
return c.search(baseDN,
return c.search(groupbaseDN,
"(&"+ groupSearchFilter + "(|(" + posixGidAttr + "={0})" +
"(" + groupMemberAttr + "={1})))",
new Object[] {gidNumber, uidNumber},
@ -350,7 +364,7 @@ private List<String> lookupGroup(SearchResult result, DirContext c,
} else {
String userDn = result.getNameInNamespace();
groupResults =
c.search(baseDN,
c.search(groupbaseDN,
"(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))",
new Object[]{userDn},
SEARCH_CONTROLS);
@ -391,7 +405,7 @@ List<String> doGetGroups(String user, int goUpHierarchy)
DirContext c = getDirContext();
// Search for the user. We'll only ever need to look at the first result
NamingEnumeration<SearchResult> results = c.search(baseDN,
NamingEnumeration<SearchResult> results = c.search(userbaseDN,
userSearchFilter, new Object[]{user}, SEARCH_CONTROLS);
// return empty list if the user can not be found.
if (!results.hasMoreElements()) {
@ -489,7 +503,7 @@ void goUpGroupHierarchy(Set<String> groupDNs,
filter.append("))");
LOG.debug("Ldap group query string: " + filter.toString());
NamingEnumeration<SearchResult> groupResults =
context.search(baseDN,
context.search(groupbaseDN,
filter.toString(),
SEARCH_CONTROLS);
while (groupResults.hasMoreElements()) {
@ -575,7 +589,20 @@ public synchronized void setConf(Configuration conf) {
conf.get(BIND_PASSWORD_FILE_KEY, BIND_PASSWORD_FILE_DEFAULT));
}
baseDN = conf.get(BASE_DN_KEY, BASE_DN_DEFAULT);
String baseDN = conf.getTrimmed(BASE_DN_KEY, BASE_DN_DEFAULT);
//User search base which defaults to base dn.
userbaseDN = conf.getTrimmed(USER_BASE_DN_KEY, baseDN);
if (LOG.isDebugEnabled()) {
LOG.debug("Usersearch baseDN: " + userbaseDN);
}
//Group search base which defaults to base dn.
groupbaseDN = conf.getTrimmed(GROUP_BASE_DN_KEY, baseDN);
if (LOG.isDebugEnabled()) {
LOG.debug("Groupsearch baseDN: " + userbaseDN);
}
groupSearchFilter =
conf.get(GROUP_SEARCH_FILTER_KEY, GROUP_SEARCH_FILTER_DEFAULT);
userSearchFilter =

View File

@ -346,6 +346,26 @@
</description>
</property>
<property>
<name>hadoop.security.group.mapping.ldap.userbase</name>
<value></value>
<description>
The search base for the LDAP connection for user search query. This is a
distinguished name, and its the root of the LDAP directory for users.
If not set, hadoop.security.group.mapping.ldap.base is used.
</description>
</property>
<property>
<name>hadoop.security.group.mapping.ldap.groupbase</name>
<value></value>
<description>
The search base for the LDAP connection for group search . This is a
distinguished name, and its the root of the LDAP directory for groups.
If not set, hadoop.security.group.mapping.ldap.base is used.
</description>
</property>
<property>
<name>hadoop.security.group.mapping.ldap.search.filter.user</name>
<value>(&amp;(objectClass=user)(sAMAccountName={0}))</value>

View File

@ -76,6 +76,7 @@ This provider supports LDAP with simple password authentication using JNDI API.
`hadoop.security.group.mapping.ldap.url` must be set. This refers to the URL of the LDAP server for resolving user groups.
`hadoop.security.group.mapping.ldap.base` configures the search base for the LDAP connection. This is a distinguished name, and will typically be the root of the LDAP directory.
Get groups for a given username first looks up the user and then looks up the groups for the user result. If the directory setup has different user and group search bases, use `hadoop.security.group.mapping.ldap.userbase` and `hadoop.security.group.mapping.ldap.groupbase` configs.
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`.

View File

@ -80,10 +80,12 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
private static final byte[] AUTHENTICATE_SUCCESS_MSG =
{48, 12, 2, 1, 1, 97, 7, 10, 1, 0, 4, 0, 4, 0};
private final String userDN = "CN=some_user,DC=test,DC=com";
@Before
public void setupMocks() throws NamingException {
when(getUserSearchResult().getNameInNamespace()).
thenReturn("CN=some_user,DC=test,DC=com");
thenReturn(userDN);
}
@Test
@ -96,6 +98,66 @@ public void testGetGroups() throws IOException, NamingException {
doTestGetGroups(Arrays.asList(getTestGroups()), 2);
}
@Test
public void testGetGroupsWithDifferentBaseDNs() throws Exception {
Configuration conf = new Configuration();
// Set this, so we don't throw an exception
conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test");
String userBaseDN = "ou=Users,dc=xxx,dc=com ";
String groupBaseDN = " ou=Groups,dc=xxx,dc=com";
conf.set(LdapGroupsMapping.USER_BASE_DN_KEY, userBaseDN);
conf.set(LdapGroupsMapping.GROUP_BASE_DN_KEY, groupBaseDN);
doTestGetGroupsWithBaseDN(conf, userBaseDN.trim(), groupBaseDN.trim());
}
@Test
public void testGetGroupsWithDefaultBaseDN() throws Exception {
Configuration conf = new Configuration();
// Set this, so we don't throw an exception
conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test");
String baseDN = " dc=xxx,dc=com ";
conf.set(LdapGroupsMapping.BASE_DN_KEY, baseDN);
doTestGetGroupsWithBaseDN(conf, baseDN.trim(), baseDN.trim());
}
/**
* Helper method to do the LDAP getGroups operation using given user base DN
* and group base DN.
* @param conf The created configuration
* @param userBaseDN user base DN
* @param groupBaseDN group base DN
* @throws NamingException if error happens when getting groups
*/
private void doTestGetGroupsWithBaseDN(Configuration conf, String userBaseDN,
String groupBaseDN) throws NamingException {
final LdapGroupsMapping groupsMapping = getGroupsMapping();
groupsMapping.setConf(conf);
final String userName = "some_user";
// The search functionality of the mock context is reused, so we will
// return the user NamingEnumeration first, and then the group
when(getContext().search(anyString(), anyString(), any(Object[].class),
any(SearchControls.class)))
.thenReturn(getUserNames(), getGroupNames());
List<String> groups = groupsMapping.getGroups(userName);
Assert.assertEquals(Arrays.asList(getTestGroups()), groups);
// We should have searched for the username and groups with default base dn
verify(getContext(), times(1)).search(userBaseDN,
LdapGroupsMapping.USER_SEARCH_FILTER_DEFAULT,
new Object[]{userName},
LdapGroupsMapping.SEARCH_CONTROLS);
verify(getContext(), times(1)).search(groupBaseDN,
"(&" + LdapGroupsMapping.GROUP_SEARCH_FILTER_DEFAULT + "(" +
LdapGroupsMapping.GROUP_MEMBERSHIP_ATTR_DEFAULT + "={0}))",
new Object[]{userDN},
LdapGroupsMapping.SEARCH_CONTROLS);
}
@Test
public void testGetGroupsWithHierarchy() throws IOException, NamingException {
// The search functionality of the mock context is reused, so we will