Skip to content

Commit c679bc7

Browse files
HADOOP-18074 - Partial/Incomplete groups list can be returned in LDAP. (#4503)
Partial/Incomplete groups list can be returned in LDAP groups lookup. Backported in #4550; minor tuning of parameters needed. Contributed by larry mccay
1 parent 7b0c2b7 commit c679bc7

File tree

2 files changed

+95
-17
lines changed

2 files changed

+95
-17
lines changed

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import javax.net.ssl.TrustManager;
5959
import javax.net.ssl.TrustManagerFactory;
6060

61+
import org.apache.hadoop.classification.VisibleForTesting;
6162
import org.apache.hadoop.thirdparty.com.google.common.collect.Iterators;
6263
import org.apache.hadoop.classification.InterfaceAudience;
6364
import org.apache.hadoop.classification.InterfaceStability;
@@ -458,7 +459,8 @@ private NamingEnumeration<SearchResult> lookupPosixGroup(SearchResult result,
458459
* @return a list of strings representing group names of the user.
459460
* @throws NamingException if unable to find group names
460461
*/
461-
private List<String> lookupGroup(SearchResult result, DirContext c,
462+
@VisibleForTesting
463+
List<String> lookupGroup(SearchResult result, DirContext c,
462464
int goUpHierarchy)
463465
throws NamingException {
464466
List<String> groups = new ArrayList<>();
@@ -510,6 +512,7 @@ private List<String> lookupGroup(SearchResult result, DirContext c,
510512
List<String> doGetGroups(String user, int goUpHierarchy)
511513
throws NamingException {
512514
DirContext c = getDirContext();
515+
List<String> groups = new ArrayList<>();
513516

514517
// Search for the user. We'll only ever need to look at the first result
515518
NamingEnumeration<SearchResult> results = c.search(userbaseDN,
@@ -518,11 +521,10 @@ List<String> doGetGroups(String user, int goUpHierarchy)
518521
if (!results.hasMoreElements()) {
519522
LOG.debug("doGetGroups({}) returned no groups because the " +
520523
"user is not found.", user);
521-
return new ArrayList<>();
524+
return groups;
522525
}
523526
SearchResult result = results.nextElement();
524527

525-
List<String> groups = null;
526528
if (useOneQuery) {
527529
try {
528530
/**
@@ -536,19 +538,20 @@ List<String> doGetGroups(String user, int goUpHierarchy)
536538
memberOfAttr + "' attribute." +
537539
"Returned user object: " + result.toString());
538540
}
539-
groups = new ArrayList<>();
540541
NamingEnumeration groupEnumeration = groupDNAttr.getAll();
541542
while (groupEnumeration.hasMore()) {
542543
String groupDN = groupEnumeration.next().toString();
543544
groups.add(getRelativeDistinguishedName(groupDN));
544545
}
545546
} catch (NamingException e) {
546547
// If the first lookup failed, fall back to the typical scenario.
548+
// In order to force the fallback, we need to reset groups collection.
549+
groups.clear();
547550
LOG.info("Failed to get groups from the first lookup. Initiating " +
548551
"the second LDAP query using the user's DN.", e);
549552
}
550553
}
551-
if (groups == null || groups.isEmpty() || goUpHierarchy > 0) {
554+
if (groups.isEmpty() || goUpHierarchy > 0) {
552555
groups = lookupGroup(result, c, goUpHierarchy);
553556
}
554557
LOG.debug("doGetGroups({}) returned {}", user, groups);

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

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

1919
package org.apache.hadoop.security;
2020

21+
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.List;
2324

2425
import javax.naming.NamingEnumeration;
2526
import javax.naming.NamingException;
2627
import javax.naming.directory.Attribute;
28+
import javax.naming.directory.DirContext;
2729
import javax.naming.directory.SearchControls;
2830
import javax.naming.directory.SearchResult;
2931

3032
import org.apache.hadoop.conf.Configuration;
3133
import org.junit.Assert;
32-
import org.junit.Before;
3334
import org.junit.Test;
35+
import org.mockito.stubbing.Stubber;
3436

3537
import static org.mockito.ArgumentMatchers.any;
3638
import static org.mockito.ArgumentMatchers.anyString;
@@ -49,48 +51,121 @@
4951
public class TestLdapGroupsMappingWithOneQuery
5052
extends TestLdapGroupsMappingBase {
5153

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

5657
NamingEnumeration<SearchResult> groupNames = getGroupNames();
5758
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);
59+
buildListOfGroupDNs(listOfDNs).when(groupNames).next();
60+
when(groupNames.hasMore()).
61+
thenReturn(true).thenReturn(true).
62+
thenReturn(true).thenReturn(false);
6563

6664
when(getAttributes().get(eq("memberOf"))).thenReturn(groupDN);
6765
}
6866

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

7797
private void doTestGetGroups(List<String> expectedGroups)
7898
throws NamingException {
99+
List<String> groupDns = new ArrayList<>();
100+
groupDns.add("CN=abc,DC=foo,DC=bar,DC=com");
101+
groupDns.add("CN=xyz,DC=foo,DC=bar,DC=com");
102+
groupDns.add("CN=sss,DC=foo,DC=bar,DC=com");
103+
104+
setupMocks(groupDns);
79105
String ldapUrl = "ldap://test";
80106
Configuration conf = getBaseConf(ldapUrl);
81107
// enable single-query lookup
82108
conf.set(LdapGroupsMapping.MEMBEROF_ATTR_KEY, "memberOf");
83109

84-
LdapGroupsMapping groupsMapping = getGroupsMapping();
110+
TestLdapGroupsMapping groupsMapping = new TestLdapGroupsMapping();
85111
groupsMapping.setConf(conf);
86112
// Username is arbitrary, since the spy is mocked to respond the same,
87113
// regardless of input
88114
List<String> groups = groupsMapping.getGroups("some_user");
89115

90116
Assert.assertEquals(expectedGroups, groups);
117+
Assert.assertFalse("Second LDAP query should NOT have been called.",
118+
groupsMapping.isSecondaryQueryCalled());
91119

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

0 commit comments

Comments
 (0)