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
This commit is contained in:
Filip Hanik 2014-06-19 11:39:56 -07:00 committed by Rob Winch
parent 8a2a1b7a5b
commit 93b863d2e5
10 changed files with 945 additions and 13 deletions

View File

@ -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";

View File

@ -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");

View File

@ -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());
}
}

View File

@ -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

View File

@ -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.

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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 + '\'' +
'}';
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}