/*
* 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 java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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;
/**
* 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>
*
* @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
*/
private 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, List<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, List<String>> record : userRoles) {
boolean circular = false;
String dn = record.get(SpringSecurityLdapTemplate.DN_KEY).get(0);
List<String> roleValues = record.get(getGroupRoleAttribute());
Set<String> roles = new HashSet<String>();
if (roleValues != null) {
roles.addAll(roleValues);
}
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
*/
private Set<String> getAttributeNames() {
return this.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
*/
private int getMaxSearchDepth() {
return this.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;
}
}