瀏覽代碼

SEC-2690: Support static nested groups in LDAP

This refers to groups that have member: <another group DN> 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
Filip Hanik 11 年之前
父節點
當前提交
93b863d2e5

+ 64 - 0
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<Map<String, String[]>> values = 
+            template.searchForMultipleAttributeValues(
+                "ou=people",
+                "(uid={0})",
+                new String[] {"bob"},
+                null);
+        assertEquals(1, values.size());
+        Map<String, String[]> record = (Map<String, String[]>)values.toArray()[0];
+        assertAttributeValue(record,"uid","bob");
+        assertAttributeValue(record,"objectclass","top","person","organizationalPerson","inetOrgPerson");
+        assertAttributeValue(record,"cn","Bob Hamilton");
+        assertAttributeValue(record,"sn","Hamilton");
+        assertFalse(record.containsKey("userPassword"));
+    }
+
+    @Test
+    public void testMultiAttributeRetrievalWithZeroLengthAttributeNames() {
+        Set<Map<String, String[]>> values =
+            template.searchForMultipleAttributeValues(
+                "ou=people",
+                "(uid={0})",
+                new String[] {"bob"},
+                new String[0]);
+        assertEquals(1, values.size());
+        Map<String, String[]> record = (Map<String, String[]>)values.toArray()[0];
+        assertAttributeValue(record,"uid","bob");
+        assertAttributeValue(record,"objectclass","top","person","organizationalPerson","inetOrgPerson");
+        assertAttributeValue(record,"cn","Bob Hamilton");
+        assertAttributeValue(record,"sn","Hamilton");
+        assertFalse(record.containsKey("userPassword"));
+    }
+
+    @Test
+    public void testMultiAttributeRetrievalWithSpecifiedAttributeNames() {
+        Set<Map<String, String[]>> values =
+            template.searchForMultipleAttributeValues(
+                "ou=people",
+                "(uid={0})",
+                new String[] {"bob"},
+                new String[] {
+                    "uid",
+                    "cn",
+                    "sn"
+                });
+        assertEquals(1, values.size());
+        Map<String, String[]> record = (Map<String, String[]>)values.toArray()[0];
+        assertAttributeValue(record,"uid","bob");
+        assertAttributeValue(record,"cn","Bob Hamilton");
+        assertAttributeValue(record,"sn","Hamilton");
+        assertFalse(record.containsKey("userPassword"));
+        assertFalse(record.containsKey("objectclass"));
+    }
+
+    protected void assertAttributeValue(Map<String, String[]> record, String attributeName, String... values) {
+        assertTrue(record.containsKey(attributeName));
+        assertEquals(values.length,record.get(attributeName).length);
+        for (int i=0; i<values.length; i++) {
+            assertEquals(values[i],record.get(attributeName)[i]);
+        }
+    }
+
     @Test
     public void testRoleSearchForMissingAttributeFailsGracefully() {
         String param = "uid=ben,ou=people,dc=springframework,dc=org";

+ 1 - 1
ldap/src/integration-test/java/org/springframework/security/ldap/search/FilterBasedLdapUserSearchTests.java

@@ -60,7 +60,7 @@ public class FilterBasedLdapUserSearchTests extends AbstractLdapIntegrationTests
     @Test
     public void extraFilterPartToExcludeBob() throws Exception {
         FilterBasedLdapUserSearch locator = new FilterBasedLdapUserSearch("ou=people",
-                "(&(cn=*)(!(|(uid={0})(uid=rod)(uid=jerry)(uid=slashguy))))", getContextSource());
+                "(&(cn=*)(!(|(uid={0})(uid=rod)(uid=jerry)(uid=slashguy)(uid=javadude)(uid=groovydude)(uid=closuredude)(uid=scaladude))))", getContextSource());
 
         // Search for bob, get back ben...
         DirContextOperations ben = locator.searchForUser("bob");

+ 134 - 0
ldap/src/integration-test/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulatorTests.java

@@ -0,0 +1,134 @@
+/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+ *
+ * Licensed 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.springframework.security.ldap.userdetails;
+
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.ldap.AbstractLdapIntegrationTests;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Filip Hanik
+ */
+public class NestedLdapAuthoritiesPopulatorTests extends AbstractLdapIntegrationTests {
+
+    private NestedLdapAuthoritiesPopulator populator;
+    private LdapAuthority javaDevelopers;
+    private LdapAuthority groovyDevelopers;
+    private LdapAuthority scalaDevelopers;
+    private LdapAuthority closureDevelopers;
+    private LdapAuthority jDevelopers;
+    private LdapAuthority circularJavaDevelopers;
+    //~ Methods ========================================================================================================
+
+    @Before
+    public void setUp() throws Exception {
+        populator = new NestedLdapAuthoritiesPopulator(getContextSource(), "ou=jdeveloper");
+        populator.setGroupSearchFilter("(member={0})");
+        populator.setIgnorePartialResultException(false);
+        populator.setRolePrefix("");
+        populator.setSearchSubtree(true);
+        populator.setConvertToUpperCase(false);
+        jDevelopers = new LdapAuthority("j-developers","cn=j-developers,ou=jdeveloper,dc=springframework,dc=org");
+        javaDevelopers = new LdapAuthority("java-developers","cn=java-developers,ou=jdeveloper,dc=springframework,dc=org");
+        groovyDevelopers = new LdapAuthority("groovy-developers","cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org");
+        scalaDevelopers = new LdapAuthority("scala-developers","cn=scala-developers,ou=jdeveloper,dc=springframework,dc=org");
+        closureDevelopers = new LdapAuthority("closure-developers","cn=closure-developers,ou=jdeveloper,dc=springframework,dc=org");
+        circularJavaDevelopers = new LdapAuthority("circular-java-developers","cn=circular-java-developers,ou=jdeveloper,dc=springframework,dc=org");
+    }
+
+    @Test
+    public void testScalaDudeJDevelopersAuthorities() {
+        DirContextAdapter ctx = new DirContextAdapter("uid=scaladude,ou=people,dc=springframework,dc=org");
+        Collection<GrantedAuthority> authorities = populator.getGrantedAuthorities(ctx,"scaladude");
+        assertEquals(5, authorities.size());
+        assertEquals(Arrays.asList(javaDevelopers, scalaDevelopers, circularJavaDevelopers, jDevelopers, groovyDevelopers), authorities);
+    }
+
+    @Test
+    public void testJavaDudeJDevelopersAuthorities() {
+        DirContextAdapter ctx = new DirContextAdapter("uid=javadude,ou=people,dc=springframework,dc=org");
+        Collection<GrantedAuthority> authorities = populator.getGrantedAuthorities(ctx,"javadude");
+        assertEquals(3, authorities.size());
+        assertEquals(Arrays.asList(javaDevelopers, circularJavaDevelopers, jDevelopers), authorities);
+    }
+
+    @Test
+    public void testScalaDudeJDevelopersAuthoritiesWithSearchLimit() {
+        populator.setMaxSearchDepth(1);
+        DirContextAdapter ctx = new DirContextAdapter("uid=scaladude,ou=people,dc=springframework,dc=org");
+        Collection<GrantedAuthority> authorities = populator.getGrantedAuthorities(ctx,"scaladude");
+        assertEquals(1, authorities.size());
+        assertEquals(Arrays.asList(scalaDevelopers), authorities);
+    }
+
+    @Test
+    public void testGroovyDudeJDevelopersAuthorities() {
+        DirContextAdapter ctx = new DirContextAdapter("uid=groovydude,ou=people,dc=springframework,dc=org");
+        Collection<GrantedAuthority> authorities = populator.getGrantedAuthorities(ctx,"groovydude");
+        assertEquals(4, authorities.size());
+        assertEquals(Arrays.asList(javaDevelopers,circularJavaDevelopers,jDevelopers,groovyDevelopers), authorities);
+    }
+
+    @Test
+    public void testClosureDudeJDevelopersWithMembershipAsAttributeValues() {
+        populator.setAttributeNames(new HashSet(Arrays.asList("member")));
+
+        DirContextAdapter ctx = new DirContextAdapter("uid=closuredude,ou=people,dc=springframework,dc=org");
+        Collection<GrantedAuthority> authorities = populator.getGrantedAuthorities(ctx,"closuredude");
+        assertEquals(5, authorities.size());
+        assertEquals(Arrays.asList(closureDevelopers,javaDevelopers,circularJavaDevelopers,jDevelopers,groovyDevelopers), authorities);
+
+        LdapAuthority[] ldapAuthorities = authorities.toArray(new LdapAuthority[0]);
+        assertEquals(5, ldapAuthorities.length);
+        //closure group
+        assertTrue(ldapAuthorities[0].getAttributes().containsKey("member"));
+        assertNotNull(ldapAuthorities[0].getAttributes().get("member"));
+        assertEquals(1, ldapAuthorities[0].getAttributes().get("member").length);
+        assertEquals("uid=closuredude,ou=people,dc=springframework,dc=org",ldapAuthorities[0].getFirstAttributeValue("member"));
+
+        //java group
+        assertTrue(ldapAuthorities[1].getAttributes().containsKey("member"));
+        assertNotNull(ldapAuthorities[1].getAttributes().get("member"));
+        assertEquals(3,ldapAuthorities[1].getAttributes().get("member").length);
+        assertEquals(groovyDevelopers.getDn(),ldapAuthorities[1].getFirstAttributeValue("member"));
+        assertEquals(
+            new String[] {
+                groovyDevelopers.getDn(),
+                scalaDevelopers.getDn(),
+                "uid=javadude,ou=people,dc=springframework,dc=org"
+            },
+            ldapAuthorities[1].getAttributes().get("member")
+        );
+
+        //test non existent attribute
+        assertNull(ldapAuthorities[2].getFirstAttributeValue("test"));
+        assertNotNull(ldapAuthorities[2].getAttributeValues("test"));
+        assertEquals(0, ldapAuthorities[2].getAttributeValues("test").length);
+        //test role name
+        assertEquals(jDevelopers.getAuthority(), ldapAuthorities[3].getAuthority());
+    }
+}

+ 106 - 0
ldap/src/integration-test/resources/test-server.ldif

@@ -122,3 +122,109 @@ objectclass: groupOfNames
 cn: submanagers
 ou: submanager
 member: uid=ben,ou=people,dc=springframework,dc=org
+
+#Nested groups data
+###################
+
+dn: ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: organizationalUnit
+ou: jdeveloper
+
+
+# javadude is part of (in a nested search)
+# circular-java-developers, java-developers, j-developers
+dn: uid=javadude,ou=people,dc=springframework,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Java Dude
+sn: Dude
+uid: javadude
+userPassword: javadudespassword
+
+# groovydude is part of (in a nested search)
+# groovy-developers, java-developers, circular-java-developers, j-developers 
+dn: uid=groovydude,ou=people,dc=springframework,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Groovy Dude
+sn: Dude
+uid: groovydude
+userPassword: groovydudespassword
+
+# closuredude is part of (in a nested search)
+# closure-developers, groovy-developers, java-developers, circular-java-developers, j-developers 
+dn: uid=closuredude,ou=people,dc=springframework,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Closure Dude
+sn: Dude
+uid: closuredude
+userPassword: closuredudespassword
+
+# scaladude is part of (in a nested search)
+# scala-developers, groovy-developers, java-developers, circular-java-developers, j-developers 
+dn: uid=scaladude,ou=people,dc=springframework,dc=org
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+cn: Scala Dude
+sn: Dude
+uid: scaladude
+userPassword: scaladudespassword
+
+dn: cn=j-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: j-developers
+ou: jdeveloper
+member: cn=java-developers,ou=jdeveloper,dc=springframework,dc=org
+
+dn: cn=java-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: java-developers
+ou: jdeveloper
+member: cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org
+member: cn=scala-developers,ou=jdeveloper,dc=springframework,dc=org
+member: uid=javadude,ou=people,dc=springframework,dc=org
+
+dn: cn=circular-java-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: circular-java-developers
+ou: jdeveloper
+member: cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org
+member: cn=scala-developers,ou=jdeveloper,dc=springframework,dc=org
+member: uid=javadude,ou=people,dc=springframework,dc=org
+
+
+dn: cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: groovy-developers
+ou: jdeveloper
+member: cn=closure-developers,ou=jdeveloper,dc=springframework,dc=org
+member: uid=groovydude,ou=people,dc=springframework,dc=org
+member: cn=circular-java-developers,ou=jdeveloper,dc=springframework,dc=org
+
+dn: cn=closure-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: closure-developers
+ou: jdeveloper
+member: uid=closuredude,ou=people,dc=springframework,dc=org
+
+dn: cn=scala-developers,ou=jdeveloper,dc=springframework,dc=org
+objectclass: top
+objectclass: groupOfNames
+cn: scala-developers
+ou: jdeveloper
+member: uid=scaladude,ou=people,dc=springframework,dc=org

+ 100 - 6
ldap/src/main/java/org/springframework/security/ldap/SpringSecurityLdapTemplate.java

@@ -30,13 +30,18 @@ import org.springframework.util.Assert;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.PartialResultException;
+import javax.naming.directory.Attribute;
 import javax.naming.directory.Attributes;
 import javax.naming.directory.DirContext;
 import javax.naming.directory.SearchControls;
 import javax.naming.directory.SearchResult;
 import java.text.MessageFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 
@@ -45,6 +50,7 @@ import java.util.Set;
  *
  * @author Ben Alex
  * @author Luke Taylor
+ * @author Filip Hanik
  * @since 2.0
  */
 public class SpringSecurityLdapTemplate extends LdapTemplate {
@@ -52,6 +58,13 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
     private static final Log logger = LogFactory.getLog(SpringSecurityLdapTemplate.class);
 
     public static final String[] NO_ATTRS = new String[0];
+    
+    /**
+     * Every search results where a record is defined by a Map&lt;String,String[]&gt;
+     * contains at least this key - the DN of the record itself.
+     */
+    public static final String DN_KEY = "spring.security.ldap.dn";
+
     private static final boolean RETURN_OBJECT = true;
 
     //~ Instance fields ================================================================================================
@@ -139,6 +152,34 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
      */
     public Set<String> searchForSingleAttributeValues(final String base, final String filter, final Object[] params,
             final String attributeName) {
+        String[] attributeNames = new String[] {attributeName};
+        Set<Map<String,String[]>> multipleAttributeValues = searchForMultipleAttributeValues(base,filter,params,attributeNames);
+        Set<String> result = new HashSet<String>();
+        for (Map<String,String[]> map : multipleAttributeValues) {
+            String[] values = map.get(attributeName);
+            if (values!=null && values.length>0) {
+                result.addAll(Arrays.asList(values));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Performs a search using the supplied filter and returns the values of each named attribute
+     * found in all entries matched by the search. Note that one directory entry may have several values for the
+     * attribute. Intended for role searches and similar scenarios.
+     *
+     * @param base the DN to search in
+     * @param filter search filter to use
+     * @param params the parameters to substitute in the search filter
+     * @param attributeNames the attributes' values that are to be retrieved.
+     *
+     * @return the set of String values for each attribute found in all the matching entries.
+     * The attribute name is the key for each set of values. In addition each map contains the DN as a String
+     * with the key predefined key {@link #DN_KEY}.
+     */
+    public Set<Map<String, String[]>> searchForMultipleAttributeValues(final String base, final String filter, final Object[] params,
+            final String[] attributeNames) {
         // Escape the params acording to RFC2254
         Object[] encodedParams = new String[params.length];
 
@@ -149,30 +190,83 @@ public class SpringSecurityLdapTemplate extends LdapTemplate {
         String formattedFilter = MessageFormat.format(filter, encodedParams);
         logger.debug("Using filter: " + formattedFilter);
 
-        final HashSet<String> set = new HashSet<String>();
+        final HashSet<Map<String, String[]>> set = new HashSet<Map<String, String[]>>();
 
         ContextMapper roleMapper = new ContextMapper() {
             public Object mapFromContext(Object ctx) {
                 DirContextAdapter adapter = (DirContextAdapter) ctx;
-                String[] values = adapter.getStringAttributes(attributeName);
-                if (values == null || values.length == 0) {
-                    logger.debug("No attribute value found for '" + attributeName + "'");
+                Map<String, String[]> record = new HashMap<String, String[]>();
+                if (attributeNames==null||attributeNames.length==0) {
+                    try {
+                        for (NamingEnumeration ae = adapter.getAttributes().getAll(); ae.hasMore(); ) {
+                            Attribute attr = (Attribute) ae.next();
+                            extractStringAttributeValues(adapter, record, attr.getID());
+                        }
+                    }catch (NamingException x) {
+                        org.springframework.ldap.support.LdapUtils.convertLdapException(x);
+                    }
                 } else {
-                    set.addAll(Arrays.asList(values));
+                    for (String attributeName : attributeNames) {
+                        extractStringAttributeValues(adapter, record, attributeName);
+                    }
                 }
+                record.put(DN_KEY, new String[] {getAdapterDN(adapter)});
+                set.add(record);
                 return null;
             }
         };
 
         SearchControls ctls = new SearchControls();
         ctls.setSearchScope(searchControls.getSearchScope());
-        ctls.setReturningAttributes(new String[] {attributeName});
+        ctls.setReturningAttributes(attributeNames!=null&&attributeNames.length>0?attributeNames:null);
 
         search(base, formattedFilter, ctls, roleMapper);
 
         return set;
     }
 
+    /**
+     * Returns the DN for the context representing this LDAP record.
+     * By default this is using {@link javax.naming.Context#getNameInNamespace()}
+     * instead of {@link org.springframework.ldap.core.DirContextAdapter#getDn()} since the 
+     * latter returns a partial DN if a base has been specified.
+     * @param adapter - the Context to extract the DN from
+     * @return - the String representing the full DN
+     */
+    protected String getAdapterDN(DirContextAdapter adapter) {
+        //returns the full DN rather than the sub DN if a base is specified
+        return adapter.getNameInNamespace();
+    }
+
+    /**
+     * Extracts String values for a specified attribute name and places them in the map representing the ldap record
+     * If a value is not of type String, it will derive it's value from the {@link Object#toString()}
+     * @param adapter - the adapter that contains the values
+     * @param record - the map holding the attribute names and values
+     * @param attributeName - the name for which to fetch the values from
+     */
+    protected void extractStringAttributeValues(DirContextAdapter adapter, Map<String, String[]> record, String attributeName) {
+        Object[] values = adapter.getObjectAttributes(attributeName);
+        if (values == null || values.length == 0) {
+            logger.debug("No attribute value found for '" + attributeName + "'");
+            return;
+        }
+        List<String> svalues = new ArrayList<String>();
+        for (Object o : values) {
+            if (o!=null) {
+                if (String.class.isAssignableFrom(o.getClass())) {
+                    svalues.add((String)o);
+                } else {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Attribute:" + attributeName + " contains a non string value of type[" + o.getClass() + "]");
+                    }
+                    svalues.add(o.toString());
+                }
+            }
+        }
+        record.put(attributeName, svalues.toArray(new String[svalues.size()]));
+    }
+
     /**
      * Performs a search, with the requirement that the search shall return a single directory entry, and uses
      * the supplied mapper to create the object from that entry.

+ 2 - 1
ldap/src/main/java/org/springframework/security/ldap/server/ApacheDSContainer.java

@@ -127,7 +127,8 @@ public class ApacheDSContainer implements InitializingBean, DisposableBean, Life
 
         server = new LdapServer();
         server.setDirectoryService(service);
-        server.setTransports(new TcpTransport(port));
+        //AbstractLdapIntegrationTests assume IPv4, so we specify the same here
+        server.setTransports(new TcpTransport("127.0.0.1", port));
         start();
     }
 

+ 86 - 5
ldap/src/main/java/org/springframework/security/ldap/userdetails/DefaultLdapAuthoritiesPopulator.java

@@ -92,6 +92,7 @@ import java.util.Set;
  * a search of the entire subtree under <tt>groupSearchBase</tt>.
  *
  * @author Luke Taylor
+ * @author Filip Hanik
  */
 public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator {
     //~ Static fields/initializers =====================================================================================
@@ -105,6 +106,9 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
      */
     private GrantedAuthority defaultRole;
 
+    /**
+     * Template that will be used for searching
+     */
     private final SpringSecurityLdapTemplate ldapTemplate;
 
     /**
@@ -127,7 +131,13 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
      * The pattern to be used for the user search. {0} is the user's DN
      */
     private String groupSearchFilter = "(member={0})";
+    /**
+     * The role prefix that will be prepended to each role name
+     */
     private String rolePrefix = "ROLE_";
+    /**
+     * Should we convert the role name to uppercase
+     */
     private boolean convertToUpperCase = true;
 
     //~ Constructors ===================================================================================================
@@ -143,7 +153,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
     public DefaultLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) {
         Assert.notNull(contextSource, "contextSource must not be null");
         ldapTemplate = new SpringSecurityLdapTemplate(contextSource);
-        ldapTemplate.setSearchControls(searchControls);
+        getLdapTemplate().setSearchControls(getSearchControls());
         this.groupSearchBase = groupSearchBase;
 
         if (groupSearchBase == null) {
@@ -212,8 +222,8 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
                     + groupSearchFilter + " in search base '" + getGroupSearchBase() + "'");
         }
 
-        Set<String> userRoles = ldapTemplate.searchForSingleAttributeValues(getGroupSearchBase(), groupSearchFilter,
-                new String[]{userDn, username}, groupRoleAttribute);
+        Set<String> userRoles = getLdapTemplate().searchForSingleAttributeValues(getGroupSearchBase(), groupSearchFilter,
+            new String[]{userDn, username}, groupRoleAttribute);
 
         if (logger.isDebugEnabled()) {
             logger.debug("Roles from search: " + userRoles);
@@ -232,7 +242,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
     }
 
     protected ContextSource getContextSource() {
-        return ldapTemplate.getContextSource();
+        return getLdapTemplate().getContextSource();
     }
 
     protected String getGroupSearchBase() {
@@ -297,6 +307,77 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
      *   @see LdapTemplate#setIgnoreNameNotFoundException(boolean)
      */
     public void setIgnorePartialResultException(boolean ignore) {
-        ldapTemplate.setIgnorePartialResultException(ignore);
+        getLdapTemplate().setIgnorePartialResultException(ignore);
+    }
+
+    /**
+     * Returns the current LDAP template.
+     * Method available so that classes extending this can override the template used 
+     * @return the LDAP template
+     * @see {@link org.springframework.security.ldap.SpringSecurityLdapTemplate}
+     */
+    protected SpringSecurityLdapTemplate getLdapTemplate() {
+        return ldapTemplate;
+    }
+
+    /**
+     * Returns the default role
+     * Method available so that classes extending this can override
+     * @return the default role used
+     * @see {@link #setDefaultRole(String)}
+     */
+    protected GrantedAuthority getDefaultRole() {
+        return defaultRole;
+    }
+
+    /**
+     * Returns the search controls
+     * Method available so that classes extending this can override the search controls used
+     * @return the search controls
+     */
+    protected SearchControls getSearchControls() {
+        return searchControls;
     }
+
+    /**
+     * Returns the attribute name of the LDAP attribute that will be mapped to the role name
+     * Method available so that classes extending this can override
+     * @return the attribute name used for role mapping
+     * @see {@link #setGroupRoleAttribute(String)}
+     */
+    protected String getGroupRoleAttribute() {
+        return groupRoleAttribute;
+    }
+
+    /**
+     * Returns the search filter configured for this populator
+     * Method available so that classes extending this can override
+     * @return the search filter
+     * @see {@link #setGroupSearchFilter(String)}
+     */
+    protected String getGroupSearchFilter() {
+        return groupSearchFilter;
+    }
+
+    /**
+     * Returns the role prefix used by this populator
+     * Method available so that classes extending this can override
+     * @return the role prefix
+     * @see {@link #setRolePrefix(String)}
+     */
+    protected String getRolePrefix() {
+        return rolePrefix;
+    }
+
+    /**
+     * Returns true if role names are converted to uppercase
+     * Method available so that classes extending this can override
+     * @return true if role names are converted to uppercase.
+     * @see {@link #setConvertToUpperCase(boolean)}
+     */
+    protected boolean isConvertToUpperCase() {
+        return convertToUpperCase;
+    }
+
+
 }

+ 142 - 0
ldap/src/main/java/org/springframework/security/ldap/userdetails/LdapAuthority.java

@@ -0,0 +1,142 @@
+/*
+ * Copyright 2002-2014 the original author or authors.
+ *
+ * Licensed 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.springframework.security.ldap.userdetails;
+
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Map;
+
+/**
+ * An authority that contains at least a DN and a role name for an LDAP entry
+ * but can also contain other desired attributes to be fetched during an LDAP
+ * authority search.
+ * @author Filip Hanik
+ */
+public class LdapAuthority implements GrantedAuthority {
+
+
+    private String dn;
+    private String role;
+    private Map<String, String[]> attributes;
+
+    /**
+     * Constructs an LdapAuthority that has a role and a DN but no other attributes
+     * @param role
+     * @param dn
+     */
+    public LdapAuthority(String role, String dn) {
+        this(role,dn,null);
+    }
+
+    /**
+     * Constructs an LdapAuthority with the given role, DN and other LDAP attributes
+     * @param role
+     * @param dn
+     * @param attributes
+     */
+    public LdapAuthority(String role, String dn, Map<String,String[]> attributes) {
+        if (role==null) throw new NullPointerException("role can not be null");
+        this.role = role;
+        this.dn = dn;
+        this.attributes = attributes;
+    }
+
+    /**
+     * Returns the LDAP attributes
+     * @return the LDAP attributes, map can be null
+     */
+    public Map<String, String[]> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Returns the DN for this LDAP authority
+     * @return
+     */
+    public String getDn() {
+        return dn;
+    }
+
+    /**
+     * Returns the values for a specific attribute
+     * @param name the attribute name
+     * @return a String array, never null but may be zero length
+     */
+    public String[] getAttributeValues(String name) {
+        String[] result = null;
+        if (attributes!=null) {
+            result = attributes.get(name);
+        }
+        if (result==null) {
+            result = new String[0];
+        }
+        return result;
+    }
+
+    /**
+     * Returns the first attribute value for a specified attribute
+     * @param name
+     * @return the first attribute value for a specified attribute, may be null
+     */
+    public String getFirstAttributeValue(String name) {
+        String[] result = getAttributeValues(name);
+        if (result.length>0) {
+            return result[0];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getAuthority() {
+        return role;
+    }
+
+    /**
+     * Compares the LdapAuthority based on {@link #getAuthority()} and {@link #getDn()} values
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof LdapAuthority)) return false;
+
+        LdapAuthority that = (LdapAuthority) o;
+
+        if (!dn.equals(that.dn)) return false;
+        if (role != null ? !role.equals(that.role) : that.role != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = dn.hashCode();
+        result = 31 * result + (role != null ? role.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "LdapAuthority{" +
+            "dn='" + dn + '\'' +
+            ", role='" + role + '\'' +
+            '}';
+    }
+}

+ 258 - 0
ldap/src/main/java/org/springframework/security/ldap/userdetails/NestedLdapAuthoritiesPopulator.java

@@ -0,0 +1,258 @@
+/*
+ * Copyright 2002-2014 the original author or authors.
+ *
+ * Licensed 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.springframework.security.ldap.userdetails;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.ldap.SpringSecurityLdapTemplate;
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A LDAP authority populator that can recursively search static nested groups.
+ * <p>An example of nested groups can be
+ * <pre>
+ *  #Nested groups data
+ *
+ *  dn: uid=javadude,ou=people,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: person
+ *  objectclass: organizationalPerson
+ *  objectclass: inetOrgPerson
+ *  cn: Java Dude
+ *  sn: Dude
+ *  uid: javadude
+ *  userPassword: javadudespassword
+ *
+ *  dn: uid=groovydude,ou=people,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: person
+ *  objectclass: organizationalPerson
+ *  objectclass: inetOrgPerson
+ *  cn: Groovy Dude
+ *  sn: Dude
+ *  uid: groovydude
+ *  userPassword: groovydudespassword
+ *
+ *  dn: uid=closuredude,ou=people,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: person
+ *  objectclass: organizationalPerson
+ *  objectclass: inetOrgPerson
+ *  cn: Closure Dude
+ *  sn: Dude
+ *  uid: closuredude
+ *  userPassword: closuredudespassword
+ *
+ *  dn: uid=scaladude,ou=people,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: person
+ *  objectclass: organizationalPerson
+ *  objectclass: inetOrgPerson
+ *  cn: Scala Dude
+ *  sn: Dude
+ *  uid: scaladude
+ *  userPassword: scaladudespassword
+ *
+ *  dn: cn=j-developers,ou=jdeveloper,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: groupOfNames
+ *  cn: j-developers
+ *  ou: jdeveloper
+ *  member: cn=java-developers,ou=groups,dc=springframework,dc=org
+ *
+ *  dn: cn=java-developers,ou=jdeveloper,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: groupOfNames
+ *  cn: java-developers
+ *  ou: jdeveloper
+ *  member: cn=groovy-developers,ou=groups,dc=springframework,dc=org
+ *  member: cn=scala-developers,ou=groups,dc=springframework,dc=org
+ *  member: uid=javadude,ou=people,dc=springframework,dc=org
+ *
+ *  dn: cn=groovy-developers,ou=jdeveloper,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: groupOfNames
+ *  cn: java-developers
+ *  ou: jdeveloper
+ *  member: cn=closure-developers,ou=groups,dc=springframework,dc=org
+ *  member: uid=groovydude,ou=people,dc=springframework,dc=org
+ *
+ *  dn: cn=closure-developers,ou=jdeveloper,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: groupOfNames
+ *  cn: java-developers
+ *  ou: jdeveloper
+ *  member: uid=closuredude,ou=people,dc=springframework,dc=org
+ *
+ *  dn: cn=scala-developers,ou=jdeveloper,dc=springframework,dc=org
+ *  objectclass: top
+ *  objectclass: groupOfNames
+ *  cn: java-developers
+ *  ou: jdeveloper
+ *  member: uid=scaladude,ou=people,dc=springframework,dc=org * </pre>
+ * </pre>
+ * </p>
+ *
+ * @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<String> attributeNames;
+
+    /**
+     * Maximum search depth - represents the number of recursive searches performed
+     */
+    private int maxSearchDepth = 10;
+    /**
+     * Constructor for group search scenarios. <tt>userRoleAttributes</tt> 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<GrantedAuthority> getGroupMembershipRoles(String userDn, String username) {
+        if (getGroupSearchBase() == null) {
+            return new HashSet<GrantedAuthority>();
+        }
+
+        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
+
+        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<GrantedAuthority> 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<String>());
+        }
+        if (StringUtils.hasText(getGroupRoleAttribute()) && !getAttributeNames().contains(getGroupRoleAttribute())) {
+            getAttributeNames().add(getGroupRoleAttribute());
+        }
+
+        Set<Map<String,String[]>> 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<String,String[]> record : userRoles) {
+            boolean circular = false;
+            String dn = record.get(SpringSecurityLdapTemplate.DN_KEY)[0];
+            String[] roleValues = record.get(getGroupRoleAttribute());
+            Set<String> roles = new HashSet<String>();
+            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<String> 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<String> 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;
+    }
+
+
+
+}

+ 52 - 0
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<String,String[]> attributes = new HashMap<String,String[]>();
+        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());
+    }
+}