From 93b863d2e560473ba78bb4a4e07f7df15d388e8e Mon Sep 17 00:00:00 2001
From: Filip Hanik
Date: Thu, 19 Jun 2014 11:39:56 -0700
Subject: [PATCH] SEC-2690: Support static nested groups in LDAP
This refers to groups that have member: as an attribute
- Add in a utility method in the SpringSecurityLdapTemplate to retrieve multiple attributes and their values from an LDAP record
- Make the DefaultLdapAuthoritiesPopulator more extensible
- Add an LdapAuthority object that holds the DN in addition to other group attributes
- Add a NestedLdapAuthoritiesPopulator to search statically nested groups
---
.../SpringSecurityLdapTemplateITests.java | 64 +++++
.../FilterBasedLdapUserSearchTests.java | 2 +-
.../NestedLdapAuthoritiesPopulatorTests.java | 134 +++++++++
.../resources/test-server.ldif | 106 +++++++
.../ldap/SpringSecurityLdapTemplate.java | 106 ++++++-
.../ldap/server/ApacheDSContainer.java | 3 +-
.../DefaultLdapAuthoritiesPopulator.java | 91 +++++-
.../ldap/userdetails/LdapAuthority.java | 142 ++++++++++
.../NestedLdapAuthoritiesPopulator.java | 258 ++++++++++++++++++
.../ldap/userdetails/LdapAuthorityTests.java | 52 ++++
10 files changed, 945 insertions(+), 13 deletions(-)
create mode 100644 ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java
create mode 100644 ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java
create mode 100644 ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java
create mode 100644 ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapAuthorityTests.java
diff --git a/ldap/src/integration-test/java/org/springframework/security/ldap/SpringSecurityLdapTemplateITests.java b/ldap/src/integration-test/java/org/springframework/security/ldap/SpringSecurityLdapTemplateITests.java
index 3cc2d51614..b4c10e5bce 100644
--- a/ldap/src/integration-test/java/org/springframework/security/ldap/SpringSecurityLdapTemplateITests.java
+++ b/ldap/src/integration-test/java/org/springframework/security/ldap/SpringSecurityLdapTemplateITests.java
@@ -17,6 +17,7 @@ package org.springframework.security.ldap;
import static org.junit.Assert.*;
+import java.util.Map;
import java.util.Set;
import javax.naming.Context;
@@ -99,6 +100,69 @@ public class SpringSecurityLdapTemplateITests extends AbstractLdapIntegrationTes
assertTrue(values.contains("submanager"));
}
+ @Test
+ public void testMultiAttributeRetrievalWithNullAttributeNames() {
+ Set
+ *
+ * @author Filip Hanik
+ */
+
+public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator {
+ private static final Log logger = LogFactory.getLog(NestedLdapAuthoritiesPopulator.class);
+
+ /**
+ * The attribute names to retrieve for each LDAP group
+ */
+ private Set attributeNames;
+
+ /**
+ * Maximum search depth - represents the number of recursive searches performed
+ */
+ private int maxSearchDepth = 10;
+ /**
+ * Constructor for group search scenarios. userRoleAttributes may still be
+ * set as a property.
+ *
+ * @param contextSource supplies the contexts used to search for user roles.
+ * @param groupSearchBase if this is an empty string the search will be performed from the root DN of the
+ */
+ public NestedLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) {
+ super(contextSource, groupSearchBase);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Set getGroupMembershipRoles(String userDn, String username) {
+ if (getGroupSearchBase() == null) {
+ return new HashSet();
+ }
+
+ Set authorities = new HashSet();
+
+ performNestedSearch(userDn, username, authorities, getMaxSearchDepth());
+
+ return authorities;
+ }
+
+ /**
+ * Performs the nested group search
+ * @param userDn - the userDN to search for, will become the group DN for subsequent searches
+ * @param username - the username of the user
+ * @param authorities - the authorities set that will be populated, must not be null
+ * @param depth - the depth remaining, when 0 recursion will end
+ */
+ protected void performNestedSearch(String userDn, String username, Set authorities, int depth) {
+ if (depth==0) {
+ //back out of recursion
+ if (logger.isDebugEnabled()) {
+ logger.debug("Search aborted, max depth reached," +
+ " for roles for user '" + username + "', DN = " + "'" + userDn + "', with filter "
+ + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'");
+ }
+ return;
+ }
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Searching for roles for user '" + username + "', DN = " + "'" + userDn + "', with filter "
+ + getGroupSearchFilter() + " in search base '" + getGroupSearchBase() + "'");
+ }
+
+ if (getAttributeNames()==null) {
+ setAttributeNames(new HashSet());
+ }
+ if (StringUtils.hasText(getGroupRoleAttribute()) && !getAttributeNames().contains(getGroupRoleAttribute())) {
+ getAttributeNames().add(getGroupRoleAttribute());
+ }
+
+ Set> userRoles = getLdapTemplate().searchForMultipleAttributeValues(
+ getGroupSearchBase(),
+ getGroupSearchFilter(),
+ new String[]{userDn, username},
+ getAttributeNames().toArray(new String[getAttributeNames().size()]));
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Roles from search: " + userRoles);
+ }
+
+ for (Map record : userRoles) {
+ boolean circular = false;
+ String dn = record.get(SpringSecurityLdapTemplate.DN_KEY)[0];
+ String[] roleValues = record.get(getGroupRoleAttribute());
+ Set roles = new HashSet();
+ roles.addAll(Arrays.asList(roleValues!=null?roleValues:new String[0]));
+ for (String role : roles) {
+ if (isConvertToUpperCase()) {
+ role = role.toUpperCase();
+ }
+ role = getRolePrefix() + role;
+ //if the group already exist, we will not search for it's parents again.
+ //this prevents a forever loop for a misconfigured ldap directory
+ circular = circular | (!authorities.add(new LdapAuthority(role,dn,record)));
+ }
+ String roleName = roles.size()>0 ? roles.iterator().next() : dn;
+ if (!circular) {
+ performNestedSearch(dn, roleName, authorities, (depth - 1));
+ }
+
+ }
+ }
+
+ /**
+ * Returns the attribute names that this populator has been configured to retrieve
+ * Value can be null, represents fetch all attributes
+ * @return the attribute names or null for all
+ */
+ public Set getAttributeNames() {
+ return attributeNames;
+ }
+
+ /**
+ * Sets the attribute names to retrieve for each ldap groups. Null means retrieve all
+ * @param attributeNames - the names of the LDAP attributes to retrieve
+ */
+ public void setAttributeNames(Set attributeNames) {
+ this.attributeNames = attributeNames;
+ }
+
+ /**
+ * How far should a nested search go. Depth is calculated in the number of levels we search up for
+ * parent groups.
+ * @return the max search depth, default is 10
+ */
+ public int getMaxSearchDepth() {
+ return maxSearchDepth;
+ }
+
+ /**
+ * How far should a nested search go. Depth is calculated in the number of levels we search up for
+ * parent groups.
+ * @param maxSearchDepth the max search depth
+ */
+ public void setMaxSearchDepth(int maxSearchDepth) {
+ this.maxSearchDepth = maxSearchDepth;
+ }
+
+
+
+}
diff --git a/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapAuthorityTests.java b/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapAuthorityTests.java
new file mode 100644
index 0000000000..4af17cfe4b
--- /dev/null
+++ b/ldap/src/test/java/org/springframework/security/ldap/userdetails/LdapAuthorityTests.java
@@ -0,0 +1,52 @@
+package org.springframework.security.ldap.userdetails;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.ldap.SpringSecurityLdapTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author Filip Hanik
+ */
+public class LdapAuthorityTests {
+
+ public static final String DN = "cn=filip,ou=Users,dc=test,dc=com";
+ LdapAuthority authority;
+
+ @Before
+ public void setUp() {
+ Map attributes = new HashMap();
+ attributes.put(SpringSecurityLdapTemplate.DN_KEY,new String[] {DN});
+ attributes.put("mail",new String[] {"filip@ldap.test.org", "filip@ldap.test2.org"});
+ authority = new LdapAuthority("testRole", DN, attributes);
+ }
+
+ @Test
+ public void testGetDn() throws Exception {
+ assertEquals(DN, authority.getDn());
+ assertNotNull(authority.getAttributeValues(SpringSecurityLdapTemplate.DN_KEY));
+ assertEquals(1, authority.getAttributeValues(SpringSecurityLdapTemplate.DN_KEY).length);
+ assertEquals(DN, authority.getFirstAttributeValue(SpringSecurityLdapTemplate.DN_KEY));
+ }
+
+ @Test
+ public void testGetAttributes() throws Exception {
+ assertNotNull(authority.getAttributes());
+ assertNotNull(authority.getAttributeValues("mail"));
+ assertEquals(2, authority.getAttributeValues("mail").length);
+ assertEquals("filip@ldap.test.org", authority.getFirstAttributeValue("mail"));
+ assertEquals("filip@ldap.test.org", authority.getAttributeValues("mail")[0]);
+ assertEquals("filip@ldap.test2.org", authority.getAttributeValues("mail")[1]);
+ }
+
+ @Test
+ public void testGetAuthority() throws Exception {
+ assertNotNull(authority.getAuthority());
+ assertEquals("testRole",authority.getAuthority());
+ }
+}
\ No newline at end of file