From a2121cb0d907be439d19cd1165a0371b37a5fe68 Mon Sep 17 00:00:00 2001 From: Mingliang Liu Date: Fri, 9 Jun 2017 14:55:07 -0700 Subject: [PATCH] HADOOP-14465. LdapGroupsMapping - support user and group search base. Contributed by Shwetha G S and Mingliang Liu --- .../hadoop/security/LdapGroupsMapping.java | 41 ++++++++++-- .../src/main/resources/core-default.xml | 20 ++++++ .../src/site/markdown/GroupsMapping.md | 1 + .../security/TestLdapGroupsMapping.java | 64 ++++++++++++++++++- 4 files changed, 118 insertions(+), 8 deletions(-) diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java index 24d07c6269f..1a184e842b3 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/LdapGroupsMapping.java @@ -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 @@ public class LdapGroupsMapping 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 @@ public class LdapGroupsMapping } else { String userDn = result.getNameInNamespace(); groupResults = - c.search(baseDN, + c.search(groupbaseDN, "(&" + groupSearchFilter + "(" + groupMemberAttr + "={0}))", new Object[]{userDn}, SEARCH_CONTROLS); @@ -391,7 +405,7 @@ public class LdapGroupsMapping DirContext c = getDirContext(); // Search for the user. We'll only ever need to look at the first result - NamingEnumeration results = c.search(baseDN, + NamingEnumeration 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 @@ public class LdapGroupsMapping filter.append("))"); LOG.debug("Ldap group query string: " + filter.toString()); NamingEnumeration groupResults = - context.search(baseDN, + context.search(groupbaseDN, filter.toString(), SEARCH_CONTROLS); while (groupResults.hasMoreElements()) { @@ -575,7 +589,20 @@ public class LdapGroupsMapping 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 = diff --git a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml index e9a4f1a2d2d..7cfb0729a86 100644 --- a/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml +++ b/hadoop-common-project/hadoop-common/src/main/resources/core-default.xml @@ -346,6 +346,26 @@ + + hadoop.security.group.mapping.ldap.userbase + + + 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. + + + + + hadoop.security.group.mapping.ldap.groupbase + + + 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. + + + hadoop.security.group.mapping.ldap.search.filter.user (&(objectClass=user)(sAMAccountName={0})) diff --git a/hadoop-common-project/hadoop-common/src/site/markdown/GroupsMapping.md b/hadoop-common-project/hadoop-common/src/site/markdown/GroupsMapping.md index b6dac8588f8..89aca16c3a2 100644 --- a/hadoop-common-project/hadoop-common/src/site/markdown/GroupsMapping.md +++ b/hadoop-common-project/hadoop-common/src/site/markdown/GroupsMapping.md @@ -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`. diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java index 9e9f5a524e1..d9f0c2ae61a 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/security/TestLdapGroupsMapping.java @@ -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 class TestLdapGroupsMapping extends TestLdapGroupsMappingBase { 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 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