Skip to content

Commit e11ba59

Browse files
authored
HADOOP-18074 - Partial/Incomplete groups list can be returned in LDAP… (#4503)
* HADOOP-18074 - Partial/Incomplete groups list can be returned in LDAP groups lookup
1 parent 9b1d357 commit e11ba59

File tree

2 files changed

+93
-13
lines changed

2 files changed

+93
-13
lines changed

hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import javax.net.ssl.TrustManager;
6060
import javax.net.ssl.TrustManagerFactory;
6161

62+
import org.apache.hadoop.classification.VisibleForTesting;
6263
import org.apache.hadoop.thirdparty.com.google.common.collect.Iterators;
6364
import org.apache.hadoop.classification.InterfaceAudience;
6465
import org.apache.hadoop.classification.InterfaceStability;
@@ -428,7 +429,8 @@ private NamingEnumeration<SearchResult> lookupPosixGroup(SearchResult result,
428429
* @return a list of strings representing group names of the user.
429430
* @throws NamingException if unable to find group names
430431
*/
431-
private Set<String> lookupGroup(SearchResult result, DirContext c,
432+
@VisibleForTesting
433+
Set<String> lookupGroup(SearchResult result, DirContext c,
432434
int goUpHierarchy)
433435
throws NamingException {
434436
Set<String> groups = new LinkedHashSet<>();
@@ -510,6 +512,8 @@ Set<String> doGetGroups(String user, int goUpHierarchy)
510512
}
511513
} catch (NamingException e) {
512514
// If the first lookup failed, fall back to the typical scenario.
515+
// In order to force the fallback, we need to reset groups collection.
516+
groups.clear();
513517
LOG.info("Failed to get groups from the first lookup. Initiating " +
514518
"the second LDAP query using the user's DN.", e);
515519
}

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMappingWithOneQuery.java

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@
1818

1919
package org.apache.hadoop.security;
2020

21+
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.List;
24+
import java.util.Set;
2325

2426
import javax.naming.NamingEnumeration;
2527
import javax.naming.NamingException;
2628
import javax.naming.directory.Attribute;
29+
import javax.naming.directory.DirContext;
2730
import javax.naming.directory.SearchControls;
2831
import javax.naming.directory.SearchResult;
2932

3033
import org.apache.hadoop.conf.Configuration;
3134
import org.junit.Assert;
32-
import org.junit.Before;
3335
import org.junit.Test;
36+
import org.mockito.stubbing.Stubber;
3437

3538
import static org.mockito.ArgumentMatchers.any;
3639
import static org.mockito.ArgumentMatchers.anyString;
@@ -49,48 +52,121 @@
4952
public class TestLdapGroupsMappingWithOneQuery
5053
extends TestLdapGroupsMappingBase {
5154

52-
@Before
53-
public void setupMocks() throws NamingException {
55+
public void setupMocks(List<String> listOfDNs) throws NamingException {
5456
Attribute groupDN = mock(Attribute.class);
5557

5658
NamingEnumeration<SearchResult> groupNames = getGroupNames();
5759
doReturn(groupNames).when(groupDN).getAll();
58-
String groupName1 = "CN=abc,DC=foo,DC=bar,DC=com";
59-
String groupName2 = "CN=xyz,DC=foo,DC=bar,DC=com";
60-
String groupName3 = "CN=sss,CN=foo,DC=bar,DC=com";
61-
doReturn(groupName1).doReturn(groupName2).doReturn(groupName3).
62-
when(groupNames).next();
63-
when(groupNames.hasMore()).thenReturn(true).thenReturn(true).
64-
thenReturn(true).thenReturn(false);
60+
buildListOfGroupDNs(listOfDNs).when(groupNames).next();
61+
when(groupNames.hasMore()).
62+
thenReturn(true).thenReturn(true).
63+
thenReturn(true).thenReturn(false);
6564

6665
when(getAttributes().get(eq("memberOf"))).thenReturn(groupDN);
6766
}
6867

68+
/**
69+
* Build and return a list of individually added group DNs such
70+
* that calls to .next() will result in a single value each time.
71+
*
72+
* @param listOfDNs
73+
* @return the stubber to use for the .when().next() call
74+
*/
75+
private Stubber buildListOfGroupDNs(List<String> listOfDNs) {
76+
Stubber stubber = null;
77+
for (String s : listOfDNs) {
78+
if (stubber != null) {
79+
stubber.doReturn(s);
80+
} else {
81+
stubber = doReturn(s);
82+
}
83+
}
84+
return stubber;
85+
}
86+
6987
@Test
7088
public void testGetGroups() throws NamingException {
7189
// given a user whose ldap query returns a user object with three "memberOf"
7290
// properties, return an array of strings representing its groups.
7391
String[] testGroups = new String[] {"abc", "xyz", "sss"};
7492
doTestGetGroups(Arrays.asList(testGroups));
93+
94+
// test fallback triggered by NamingException
95+
doTestGetGroupsWithFallback();
7596
}
7697

7798
private void doTestGetGroups(List<String> expectedGroups)
7899
throws NamingException {
100+
List<String> groupDns = new ArrayList<>();
101+
groupDns.add("CN=abc,DC=foo,DC=bar,DC=com");
102+
groupDns.add("CN=xyz,DC=foo,DC=bar,DC=com");
103+
groupDns.add("CN=sss,DC=foo,DC=bar,DC=com");
104+
105+
setupMocks(groupDns);
79106
String ldapUrl = "ldap://test";
80107
Configuration conf = getBaseConf(ldapUrl);
81108
// enable single-query lookup
82109
conf.set(LdapGroupsMapping.MEMBEROF_ATTR_KEY, "memberOf");
83110

84-
LdapGroupsMapping groupsMapping = getGroupsMapping();
111+
TestLdapGroupsMapping groupsMapping = new TestLdapGroupsMapping();
85112
groupsMapping.setConf(conf);
86113
// Username is arbitrary, since the spy is mocked to respond the same,
87114
// regardless of input
88115
List<String> groups = groupsMapping.getGroups("some_user");
89116

90117
Assert.assertEquals(expectedGroups, groups);
118+
Assert.assertFalse("Second LDAP query should NOT have been called.",
119+
groupsMapping.isSecondaryQueryCalled());
91120

92121
// We should have only made one query because single-query lookup is enabled
93122
verify(getContext(), times(1)).search(anyString(), anyString(),
94123
any(Object[].class), any(SearchControls.class));
95124
}
96-
}
125+
126+
private void doTestGetGroupsWithFallback()
127+
throws NamingException {
128+
List<String> groupDns = new ArrayList<>();
129+
groupDns.add("CN=abc,DC=foo,DC=bar,DC=com");
130+
groupDns.add("CN=xyz,DC=foo,DC=bar,DC=com");
131+
groupDns.add("ipaUniqueID=e4a9a634-bb24-11ec-aec1-06ede52b5fe1," +
132+
"CN=sudo,DC=foo,DC=bar,DC=com");
133+
setupMocks(groupDns);
134+
String ldapUrl = "ldap://test";
135+
Configuration conf = getBaseConf(ldapUrl);
136+
// enable single-query lookup
137+
conf.set(LdapGroupsMapping.MEMBEROF_ATTR_KEY, "memberOf");
138+
conf.set(LdapGroupsMapping.LDAP_NUM_ATTEMPTS_KEY, "1");
139+
140+
TestLdapGroupsMapping groupsMapping = new TestLdapGroupsMapping();
141+
groupsMapping.setConf(conf);
142+
// Username is arbitrary, since the spy is mocked to respond the same,
143+
// regardless of input
144+
List<String> groups = groupsMapping.getGroups("some_user");
145+
146+
// expected to be empty due to invalid memberOf
147+
Assert.assertEquals(0, groups.size());
148+
149+
// expect secondary query to be called: getGroups()
150+
Assert.assertTrue("Second LDAP query should have been called.",
151+
groupsMapping.isSecondaryQueryCalled());
152+
153+
// We should have fallen back to the second query because first threw
154+
// NamingException expected count is 3 since testGetGroups calls
155+
// doTestGetGroups and doTestGetGroupsWithFallback in succession and
156+
// the count is across both test scenarios.
157+
verify(getContext(), times(3)).search(anyString(), anyString(),
158+
any(Object[].class), any(SearchControls.class));
159+
}
160+
161+
private static final class TestLdapGroupsMapping extends LdapGroupsMapping {
162+
private boolean secondaryQueryCalled = false;
163+
public boolean isSecondaryQueryCalled() {
164+
return secondaryQueryCalled;
165+
}
166+
Set<String> lookupGroup(SearchResult result, DirContext c,
167+
int goUpHierarchy) throws NamingException {
168+
secondaryQueryCalled = true;
169+
return super.lookupGroup(result, c, goUpHierarchy);
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)