HADOOP-12782. Faster LDAP group name resolution with ActiveDirectory. Contributed by Wei-Chiu Chuang

This commit is contained in:
Kai Zheng 2016-05-19 07:15:52 -07:00
parent d4274c64bc
commit 182fc1986a
7 changed files with 394 additions and 97 deletions

View File

@ -34,6 +34,8 @@ import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext; import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult; import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import org.apache.commons.io.Charsets; import org.apache.commons.io.Charsets;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -135,6 +137,13 @@ public class LdapGroupsMapping
public static final String GROUP_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.group"; public static final String GROUP_SEARCH_FILTER_KEY = LDAP_CONFIG_PREFIX + ".search.filter.group";
public static final String GROUP_SEARCH_FILTER_DEFAULT = "(objectClass=group)"; public static final String GROUP_SEARCH_FILTER_DEFAULT = "(objectClass=group)";
/*
* LDAP attribute to use for determining group membership
*/
public static final String MEMBEROF_ATTR_KEY =
LDAP_CONFIG_PREFIX + ".search.attr.memberof";
public static final String MEMBEROF_ATTR_DEFAULT = "";
/* /*
* LDAP attribute to use for determining group membership * LDAP attribute to use for determining group membership
*/ */
@ -189,11 +198,13 @@ public class LdapGroupsMapping
private String baseDN; private String baseDN;
private String groupSearchFilter; private String groupSearchFilter;
private String userSearchFilter; private String userSearchFilter;
private String memberOfAttr;
private String groupMemberAttr; private String groupMemberAttr;
private String groupNameAttr; private String groupNameAttr;
private String posixUidAttr; private String posixUidAttr;
private String posixGidAttr; private String posixGidAttr;
private boolean isPosix; private boolean isPosix;
private boolean useOneQuery;
public static final int RECONNECT_RETRY_COUNT = 3; public static final int RECONNECT_RETRY_COUNT = 3;
@ -230,57 +241,172 @@ public class LdapGroupsMapping
return Collections.emptyList(); return Collections.emptyList();
} }
List<String> doGetGroups(String user) throws NamingException { /**
List<String> groups = new ArrayList<String>(); * A helper method to get the Relative Distinguished Name (RDN) from
* Distinguished name (DN). According to Active Directory documentation,
* a group object's RDN is a CN.
*
* @param distinguishedName A string representing a distinguished name.
* @throws NamingException if the DN is malformed.
* @return a string which represents the RDN
*/
private String getRelativeDistinguishedName(String distinguishedName)
throws NamingException {
LdapName ldn = new LdapName(distinguishedName);
List<Rdn> rdns = ldn.getRdns();
if (rdns.isEmpty()) {
throw new NamingException("DN is empty");
}
Rdn rdn = rdns.get(rdns.size()-1);
if (rdn.getType().equalsIgnoreCase(groupNameAttr)) {
String groupName = (String)rdn.getValue();
return groupName;
}
throw new NamingException("Unable to find RDN: The DN " +
distinguishedName + " is malformed.");
}
DirContext ctx = getDirContext(); /**
* Look up groups using posixGroups semantics. Use posix gid/uid to find
// Search for the user. We'll only ever need to look at the first result * groups of the user.
NamingEnumeration<SearchResult> results = ctx.search(baseDN, *
userSearchFilter, * @param result the result object returned from the prior user lookup.
new Object[]{user}, * @param c the context object of the LDAP connection.
SEARCH_CONTROLS); * @return an object representing the search result.
if (results.hasMoreElements()) { *
SearchResult result = results.nextElement(); * @throws NamingException if the server does not support posixGroups
String userDn = result.getNameInNamespace(); * semantics.
*/
NamingEnumeration<SearchResult> groupResults = null; private NamingEnumeration<SearchResult> lookupPosixGroup(SearchResult result,
DirContext c) throws NamingException {
if (isPosix) {
String gidNumber = null; String gidNumber = null;
String uidNumber = null; String uidNumber = null;
Attribute gidAttribute = result.getAttributes().get(posixGidAttr); Attribute gidAttribute = result.getAttributes().get(posixGidAttr);
Attribute uidAttribute = result.getAttributes().get(posixUidAttr); Attribute uidAttribute = result.getAttributes().get(posixUidAttr);
if (gidAttribute != null) { String reason = "";
if (gidAttribute == null) {
reason = "Can't find attribute '" + posixGidAttr + "'.";
} else {
gidNumber = gidAttribute.get().toString(); gidNumber = gidAttribute.get().toString();
} }
if (uidAttribute != null) { if (uidAttribute == null) {
reason = "Can't find attribute '" + posixUidAttr + "'.";
} else {
uidNumber = uidAttribute.get().toString(); uidNumber = uidAttribute.get().toString();
} }
if (uidNumber != null && gidNumber != null) { if (uidNumber != null && gidNumber != null) {
groupResults = return c.search(baseDN,
ctx.search(baseDN,
"(&"+ groupSearchFilter + "(|(" + posixGidAttr + "={0})" + "(&"+ groupSearchFilter + "(|(" + posixGidAttr + "={0})" +
"(" + groupMemberAttr + "={1})))", "(" + groupMemberAttr + "={1})))",
new Object[] {gidNumber, uidNumber}, new Object[] {gidNumber, uidNumber},
SEARCH_CONTROLS); SEARCH_CONTROLS);
} }
throw new NamingException("The server does not support posixGroups " +
"semantics. Reason: " + reason +
" Returned user object: " + result.toString());
}
/**
* Perform the second query to get the groups of the user.
*
* If posixGroups is enabled, use use posix gid/uid to find.
* Otherwise, use the general group member attribute to find it.
*
* @param result the result object returned from the prior user lookup.
* @param c the context object of the LDAP connection.
* @return a list of strings representing group names of the user.
* @throws NamingException if unable to find group names
*/
private List<String> lookupGroup(SearchResult result, DirContext c)
throws NamingException {
List<String> groups = new ArrayList<String>();
NamingEnumeration<SearchResult> groupResults = null;
// perform the second LDAP query
if (isPosix) {
groupResults = lookupPosixGroup(result, c);
} else { } else {
String userDn = result.getNameInNamespace();
groupResults = groupResults =
ctx.search(baseDN, c.search(baseDN,
"(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))", "(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))",
new Object[]{userDn}, new Object[]{userDn},
SEARCH_CONTROLS); SEARCH_CONTROLS);
} }
// if the second query is successful, group objects of the user will be
// returned. Get group names from the returned objects.
if (groupResults != null) { if (groupResults != null) {
while (groupResults.hasMoreElements()) { while (groupResults.hasMoreElements()) {
SearchResult groupResult = groupResults.nextElement(); SearchResult groupResult = groupResults.nextElement();
Attribute groupName = groupResult.getAttributes().get(groupNameAttr); Attribute groupName = groupResult.getAttributes().get(groupNameAttr);
if (groupName == null) {
throw new NamingException("The group object does not have " +
"attribute '" + groupNameAttr + "'.");
}
groups.add(groupName.get().toString()); groups.add(groupName.get().toString());
} }
} }
return groups;
} }
/**
* Perform LDAP queries to get group names of a user.
*
* Perform the first LDAP query to get the user object using the user's name.
* If one-query is enabled, retrieve the group names from the user object.
* If one-query is disabled, or if it failed, perform the second query to
* get the groups.
*
* @param user user name
* @return a list of group names for the user. If the user can not be found,
* return an empty string array.
* @throws NamingException if unable to get group names
*/
List<String> doGetGroups(String user) throws NamingException {
DirContext c = getDirContext();
// Search for the user. We'll only ever need to look at the first result
NamingEnumeration<SearchResult> results = c.search(baseDN,
userSearchFilter, new Object[]{user}, SEARCH_CONTROLS);
// return empty list if the user can not be found.
if (!results.hasMoreElements()) {
if (LOG.isDebugEnabled()) {
LOG.debug("doGetGroups(" + user + ") return no groups because the " +
"user is not found.");
}
return new ArrayList<String>();
}
SearchResult result = results.nextElement();
List<String> groups = null;
if (useOneQuery) {
try {
/**
* For Active Directory servers, the user object has an attribute
* 'memberOf' that represents the DNs of group objects to which the
* user belongs. So the second query may be skipped.
*/
Attribute groupDNAttr = result.getAttributes().get(memberOfAttr);
if (groupDNAttr == null) {
throw new NamingException("The user object does not have '" +
memberOfAttr + "' attribute." +
"Returned user object: " + result.toString());
}
groups = new ArrayList<String>();
NamingEnumeration groupEnumeration = groupDNAttr.getAll();
while (groupEnumeration.hasMore()) {
String groupDN = groupEnumeration.next().toString();
groups.add(getRelativeDistinguishedName(groupDN));
}
} catch (NamingException e) {
// If the first lookup failed, fall back to the typical scenario.
LOG.info("Failed to get groups from the first lookup. Initiating " +
"the second LDAP query using the user's DN.", e);
}
}
if (groups == null || groups.isEmpty()) {
groups = lookupGroup(result, c);
}
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("doGetGroups(" + user + ") return " + groups); LOG.debug("doGetGroups(" + user + ") return " + groups);
} }
@ -366,6 +492,11 @@ public class LdapGroupsMapping
conf.get(USER_SEARCH_FILTER_KEY, USER_SEARCH_FILTER_DEFAULT); conf.get(USER_SEARCH_FILTER_KEY, USER_SEARCH_FILTER_DEFAULT);
isPosix = groupSearchFilter.contains(POSIX_GROUP) && userSearchFilter isPosix = groupSearchFilter.contains(POSIX_GROUP) && userSearchFilter
.contains(POSIX_ACCOUNT); .contains(POSIX_ACCOUNT);
memberOfAttr =
conf.get(MEMBEROF_ATTR_KEY, MEMBEROF_ATTR_DEFAULT);
// if memberOf attribute is set, resolve group names from the attribute
// of user objects.
useOneQuery = !memberOfAttr.isEmpty();
groupMemberAttr = groupMemberAttr =
conf.get(GROUP_MEMBERSHIP_ATTR_KEY, GROUP_MEMBERSHIP_ATTR_DEFAULT); conf.get(GROUP_MEMBERSHIP_ATTR_KEY, GROUP_MEMBERSHIP_ATTR_DEFAULT);
groupNameAttr = groupNameAttr =
@ -379,8 +510,15 @@ public class LdapGroupsMapping
SEARCH_CONTROLS.setTimeLimit(dirSearchTimeout); SEARCH_CONTROLS.setTimeLimit(dirSearchTimeout);
// Limit the attributes returned to only those required to speed up the search. // Limit the attributes returned to only those required to speed up the search.
// See HADOOP-10626 and HADOOP-12001 for more details. // See HADOOP-10626 and HADOOP-12001 for more details.
SEARCH_CONTROLS.setReturningAttributes( String[] returningAttributes;
new String[] {groupNameAttr, posixUidAttr, posixGidAttr}); if (useOneQuery) {
returningAttributes = new String[] {
groupNameAttr, posixUidAttr, posixGidAttr, memberOfAttr};
} else {
returningAttributes = new String[] {
groupNameAttr, posixUidAttr, posixGidAttr};
}
SEARCH_CONTROLS.setReturningAttributes(returningAttributes);
this.conf = conf; this.conf = conf;
} }

View File

@ -260,6 +260,18 @@
</description> </description>
</property> </property>
<property>
<name>hadoop.security.group.mapping.ldap.search.attr.memberof</name>
<value></value>
<description>
The attribute of the user object that identifies its group objects. By
default, Hadoop makes two LDAP queries per user if this value is empty. If
set, Hadoop will attempt to resolve group names from this attribute,
instead of making the second LDAP query to get group objects. The value
should be 'memberOf' for an MS AD installation.
</description>
</property>
<property> <property>
<name>hadoop.security.group.mapping.ldap.search.attr.member</name> <name>hadoop.security.group.mapping.ldap.search.attr.member</name>
<value>member</value> <value>member</value>

View File

@ -98,6 +98,12 @@ To secure the connection, the implementation supports LDAP over SSL (LDAPS). SSL
In addition, specify the path to the keystore file for SSL connection in `hadoop.security.group.mapping.ldap.ssl.keystore` and keystore password in `hadoop.security.group.mapping.ldap.ssl.keystore.password`. In addition, specify the path to the keystore file for SSL connection in `hadoop.security.group.mapping.ldap.ssl.keystore` and keystore password in `hadoop.security.group.mapping.ldap.ssl.keystore.password`.
Alternatively, store the keystore password in a file, and point `hadoop.security.group.mapping.ldap.ssl.keystore.password.file` to that file. For security purposes, this file should be readable only by the Unix user running the daemons. Alternatively, store the keystore password in a file, and point `hadoop.security.group.mapping.ldap.ssl.keystore.password.file` to that file. For security purposes, this file should be readable only by the Unix user running the daemons.
### Low latency group mapping resolution ###
Typically, Hadoop resolves a user's group names by making two LDAP queries: the first query gets the user object, and the second query uses the user's Distinguished Name to find the groups.
For some LDAP servers, such as Active Directory, the user object returned in the first query also contains the DN of the user's groups in its `memberOf` attribute, and the name of a group is its Relative Distinguished Name.
Therefore, it is possible to infer the user's groups from the first query without sending the second one, and it may reduce group name resolution latency incurred by the second query. If it fails to get group names, it will fall back to the typical two-query scenario and send the second query to get group names.
To enable this feature, set `hadoop.security.group.mapping.ldap.search.attr.memberof` to `memberOf`, and Hadoop will resolve group names using this attribute in the user object.
Composite Groups Mapping Composite Groups Mapping
-------- --------
`CompositeGroupsMapping` works by enumerating a list of service providers in `hadoop.security.group.mapping.providers`. `CompositeGroupsMapping` works by enumerating a list of service providers in `hadoop.security.group.mapping.providers`.

View File

@ -19,7 +19,11 @@ package org.apache.hadoop.security;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
@ -31,7 +35,6 @@ import java.util.List;
import javax.naming.CommunicationException; import javax.naming.CommunicationException;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.Path;
@ -47,18 +50,17 @@ import org.junit.Test;
public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase { public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
@Before @Before
public void setupMocks() throws NamingException { public void setupMocks() throws NamingException {
SearchResult mockUserResult = mock(SearchResult.class); when(getUserSearchResult().getNameInNamespace()).
when(mockUserNamingEnum.nextElement()).thenReturn(mockUserResult); thenReturn("CN=some_user,DC=test,DC=com");
when(mockUserResult.getNameInNamespace()).thenReturn("CN=some_user,DC=test,DC=com");
} }
@Test @Test
public void testGetGroups() throws IOException, NamingException { public void testGetGroups() throws IOException, NamingException {
// The search functionality of the mock context is reused, so we will // The search functionality of the mock context is reused, so we will
// return the user NamingEnumeration first, and then the group // return the user NamingEnumeration first, and then the group
when(mockContext.search(anyString(), anyString(), any(Object[].class), when(getContext().search(anyString(), anyString(), any(Object[].class),
any(SearchControls.class))) any(SearchControls.class)))
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum); .thenReturn(getUserNames(), getGroupNames());
doTestGetGroups(Arrays.asList(testGroups), 2); doTestGetGroups(Arrays.asList(testGroups), 2);
} }
@ -67,10 +69,10 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
public void testGetGroupsWithConnectionClosed() throws IOException, NamingException { public void testGetGroupsWithConnectionClosed() throws IOException, NamingException {
// The case mocks connection is closed/gc-ed, so the first search call throws CommunicationException, // The case mocks connection is closed/gc-ed, so the first search call throws CommunicationException,
// then after reconnected return the user NamingEnumeration first, and then the group // then after reconnected return the user NamingEnumeration first, and then the group
when(mockContext.search(anyString(), anyString(), any(Object[].class), when(getContext().search(anyString(), anyString(), any(Object[].class),
any(SearchControls.class))) any(SearchControls.class)))
.thenThrow(new CommunicationException("Connection is closed")) .thenThrow(new CommunicationException("Connection is closed"))
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum); .thenReturn(getUserNames(), getGroupNames());
// Although connection is down but after reconnected it still should retrieve the result groups // Although connection is down but after reconnected it still should retrieve the result groups
doTestGetGroups(Arrays.asList(testGroups), 1 + 2); // 1 is the first failure call doTestGetGroups(Arrays.asList(testGroups), 1 + 2); // 1 is the first failure call
@ -79,7 +81,7 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
@Test @Test
public void testGetGroupsWithLdapDown() throws IOException, NamingException { public void testGetGroupsWithLdapDown() throws IOException, NamingException {
// This mocks the case where Ldap server is down, and always throws CommunicationException // This mocks the case where Ldap server is down, and always throws CommunicationException
when(mockContext.search(anyString(), anyString(), any(Object[].class), when(getContext().search(anyString(), anyString(), any(Object[].class),
any(SearchControls.class))) any(SearchControls.class)))
.thenThrow(new CommunicationException("Connection is closed")); .thenThrow(new CommunicationException("Connection is closed"));
@ -93,15 +95,16 @@ public class TestLdapGroupsMapping extends TestLdapGroupsMappingBase {
// Set this, so we don't throw an exception // Set this, so we don't throw an exception
conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test"); conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test");
mappingSpy.setConf(conf); LdapGroupsMapping groupsMapping = getGroupsMapping();
groupsMapping.setConf(conf);
// Username is arbitrary, since the spy is mocked to respond the same, // Username is arbitrary, since the spy is mocked to respond the same,
// regardless of input // regardless of input
List<String> groups = mappingSpy.getGroups("some_user"); List<String> groups = groupsMapping.getGroups("some_user");
Assert.assertEquals(expectedGroups, groups); Assert.assertEquals(expectedGroups, groups);
// We should have searched for a user, and then two groups // We should have searched for a user, and then two groups
verify(mockContext, times(searchTimes)).search(anyString(), verify(getContext(), times(searchTimes)).search(anyString(),
anyString(), anyString(),
any(Object[].class), any(Object[].class),
any(SearchControls.class)); any(SearchControls.class));

View File

@ -20,7 +20,6 @@ package org.apache.hadoop.security;
import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import javax.naming.NamingEnumeration; import javax.naming.NamingEnumeration;
@ -30,34 +29,49 @@ import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes; import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext; import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult; import javax.naming.directory.SearchResult;
import org.junit.Before; import org.junit.Before;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
public class TestLdapGroupsMappingBase { public class TestLdapGroupsMappingBase {
protected DirContext mockContext; @Mock
private DirContext context;
@Mock
private NamingEnumeration<SearchResult> userNames;
@Mock
private NamingEnumeration<SearchResult> groupNames;
@Mock
private SearchResult userSearchResult;
@Mock
private Attributes attributes;
@Spy
private LdapGroupsMapping groupsMapping = new LdapGroupsMapping();
protected LdapGroupsMapping mappingSpy = spy(new LdapGroupsMapping());
protected NamingEnumeration mockUserNamingEnum =
mock(NamingEnumeration.class);
protected NamingEnumeration mockGroupNamingEnum =
mock(NamingEnumeration.class);
protected String[] testGroups = new String[] {"group1", "group2"}; protected String[] testGroups = new String[] {"group1", "group2"};
@Before @Before
public void setupMocksBase() throws NamingException { public void setupMocksBase() throws NamingException {
mockContext = mock(DirContext.class); MockitoAnnotations.initMocks(this);
doReturn(mockContext).when(mappingSpy).getDirContext(); DirContext ctx = getContext();
doReturn(ctx).when(groupsMapping).getDirContext();
when(ctx.search(Mockito.anyString(), Mockito.anyString(),
Mockito.any(Object[].class), Mockito.any(SearchControls.class))).
thenReturn(userNames);
// We only ever call hasMoreElements once for the user NamingEnum, so // We only ever call hasMoreElements once for the user NamingEnum, so
// we can just have one return value // we can just have one return value
when(mockUserNamingEnum.hasMoreElements()).thenReturn(true); when(userNames.hasMoreElements()).thenReturn(true);
SearchResult mockGroupResult = mock(SearchResult.class); SearchResult groupSearchResult = mock(SearchResult.class);
// We're going to have to define the loop here. We want two iterations, // We're going to have to define the loop here. We want two iterations,
// to get both the groups // to get both the groups
when(mockGroupNamingEnum.hasMoreElements()).thenReturn(true, true, false); when(groupNames.hasMoreElements()).thenReturn(true, true, false);
when(mockGroupNamingEnum.nextElement()).thenReturn(mockGroupResult); when(groupNames.nextElement()).thenReturn(groupSearchResult);
// Define the attribute for the name of the first group // Define the attribute for the name of the first group
Attribute group1Attr = new BasicAttribute("cn"); Attribute group1Attr = new BasicAttribute("cn");
@ -72,6 +86,35 @@ public class TestLdapGroupsMappingBase {
group2Attrs.put(group2Attr); group2Attrs.put(group2Attr);
// This search result gets reused, so return group1, then group2 // This search result gets reused, so return group1, then group2
when(mockGroupResult.getAttributes()).thenReturn(group1Attrs, group2Attrs); when(groupSearchResult.getAttributes()).
thenReturn(group1Attrs, group2Attrs);
when(getUserNames().nextElement()).
thenReturn(getUserSearchResult());
when(getUserSearchResult().getAttributes()).thenReturn(getAttributes());
}
protected DirContext getContext() {
return context;
}
protected NamingEnumeration<SearchResult> getUserNames() {
return userNames;
}
protected NamingEnumeration<SearchResult> getGroupNames() {
return groupNames;
}
protected SearchResult getUserSearchResult() {
return userSearchResult;
}
protected Attributes getAttributes() {
return attributes;
}
protected LdapGroupsMapping getGroupsMapping() {
return groupsMapping;
} }
} }

View File

@ -0,0 +1,100 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.security;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.hadoop.conf.Configuration;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Test LdapGroupsMapping with one-query lookup enabled.
* Mockito is used to simulate the LDAP server response.
*/
@SuppressWarnings("unchecked")
public class TestLdapGroupsMappingWithOneQuery
extends TestLdapGroupsMappingBase {
@Before
public void setupMocks() throws NamingException {
Attribute groupDN = mock(Attribute.class);
NamingEnumeration<SearchResult> groupNames = getGroupNames();
doReturn(groupNames).when(groupDN).getAll();
String groupName1 = "CN=abc,DC=foo,DC=bar,DC=com";
String groupName2 = "CN=xyz,DC=foo,DC=bar,DC=com";
String groupName3 = "CN=sss,CN=foo,DC=bar,DC=com";
doReturn(groupName1).doReturn(groupName2).doReturn(groupName3).
when(groupNames).next();
when(groupNames.hasMore()).thenReturn(true).thenReturn(true).
thenReturn(true).thenReturn(false);
when(getAttributes().get(eq("memberOf"))).thenReturn(groupDN);
}
@Test
public void testGetGroups() throws IOException, NamingException {
// given a user whose ldap query returns a user object with three "memberOf"
// properties, return an array of strings representing its groups.
String[] testGroups = new String[] {"abc", "xyz", "sss"};
doTestGetGroups(Arrays.asList(testGroups));
}
private void doTestGetGroups(List<String> expectedGroups)
throws IOException, NamingException {
Configuration conf = new Configuration();
// Set this, so we don't throw an exception
conf.set(LdapGroupsMapping.LDAP_URL_KEY, "ldap://test");
// enable single-query lookup
conf.set(LdapGroupsMapping.MEMBEROF_ATTR_KEY, "memberOf");
LdapGroupsMapping groupsMapping = getGroupsMapping();
groupsMapping.setConf(conf);
// Username is arbitrary, since the spy is mocked to respond the same,
// regardless of input
List<String> groups = groupsMapping.getGroups("some_user");
Assert.assertEquals(expectedGroups, groups);
// We should have only made one query because single-query lookup is enabled
verify(getContext(), times(1)).search(anyString(),
anyString(),
any(Object[].class),
any(SearchControls.class));
}
}

View File

@ -36,7 +36,6 @@ import javax.naming.NamingException;
import javax.naming.directory.Attribute; import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes; import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls; import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configuration;
import org.junit.Assert; import org.junit.Assert;
@ -49,31 +48,26 @@ public class TestLdapGroupsMappingWithPosixGroup
@Before @Before
public void setupMocks() throws NamingException { public void setupMocks() throws NamingException {
SearchResult mockUserResult = mock(SearchResult.class); Attribute uidNumberAttr = mock(Attribute.class);
when(mockUserNamingEnum.nextElement()).thenReturn(mockUserResult); Attribute gidNumberAttr = mock(Attribute.class);
Attribute uidAttr = mock(Attribute.class);
Attributes attributes = getAttributes();
Attribute mockUidNumberAttr = mock(Attribute.class); when(uidAttr.get()).thenReturn("some_user");
Attribute mockGidNumberAttr = mock(Attribute.class); when(uidNumberAttr.get()).thenReturn("700");
Attribute mockUidAttr = mock(Attribute.class); when(gidNumberAttr.get()).thenReturn("600");
Attributes mockAttrs = mock(Attributes.class); when(attributes.get(eq("uid"))).thenReturn(uidAttr);
when(attributes.get(eq("uidNumber"))).thenReturn(uidNumberAttr);
when(mockUidAttr.get()).thenReturn("some_user"); when(attributes.get(eq("gidNumber"))).thenReturn(gidNumberAttr);
when(mockUidNumberAttr.get()).thenReturn("700");
when(mockGidNumberAttr.get()).thenReturn("600");
when(mockAttrs.get(eq("uid"))).thenReturn(mockUidAttr);
when(mockAttrs.get(eq("uidNumber"))).thenReturn(mockUidNumberAttr);
when(mockAttrs.get(eq("gidNumber"))).thenReturn(mockGidNumberAttr);
when(mockUserResult.getAttributes()).thenReturn(mockAttrs);
} }
@Test @Test
public void testGetGroups() throws IOException, NamingException { public void testGetGroups() throws IOException, NamingException {
// The search functionality of the mock context is reused, so we will // The search functionality of the mock context is reused, so we will
// return the user NamingEnumeration first, and then the group // return the user NamingEnumeration first, and then the group
when(mockContext.search(anyString(), contains("posix"), when(getContext().search(anyString(), contains("posix"),
any(Object[].class), any(SearchControls.class))) any(Object[].class), any(SearchControls.class)))
.thenReturn(mockUserNamingEnum, mockGroupNamingEnum); .thenReturn(getUserNames(), getGroupNames());
doTestGetGroups(Arrays.asList(testGroups), 2); doTestGetGroups(Arrays.asList(testGroups), 2);
} }
@ -92,19 +86,20 @@ public class TestLdapGroupsMappingWithPosixGroup
conf.set(LdapGroupsMapping.POSIX_GID_ATTR_KEY, "gidNumber"); conf.set(LdapGroupsMapping.POSIX_GID_ATTR_KEY, "gidNumber");
conf.set(LdapGroupsMapping.GROUP_NAME_ATTR_KEY, "cn"); conf.set(LdapGroupsMapping.GROUP_NAME_ATTR_KEY, "cn");
mappingSpy.setConf(conf); LdapGroupsMapping groupsMapping = getGroupsMapping();
groupsMapping.setConf(conf);
// Username is arbitrary, since the spy is mocked to respond the same, // Username is arbitrary, since the spy is mocked to respond the same,
// regardless of input // regardless of input
List<String> groups = mappingSpy.getGroups("some_user"); List<String> groups = groupsMapping.getGroups("some_user");
Assert.assertEquals(expectedGroups, groups); Assert.assertEquals(expectedGroups, groups);
mappingSpy.getConf().set(LdapGroupsMapping.POSIX_UID_ATTR_KEY, "uid"); groupsMapping.getConf().set(LdapGroupsMapping.POSIX_UID_ATTR_KEY, "uid");
Assert.assertEquals(expectedGroups, groups); Assert.assertEquals(expectedGroups, groups);
// We should have searched for a user, and then two groups // We should have searched for a user, and then two groups
verify(mockContext, times(searchTimes)).search(anyString(), verify(getContext(), times(searchTimes)).search(anyString(),
anyString(), anyString(),
any(Object[].class), any(Object[].class),
any(SearchControls.class)); any(SearchControls.class));