/* * 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.cloudfoundry.identity.uaa.provider.ldap.extension; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; import org.springframework.util.StringUtils; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import static java.util.Collections.EMPTY_LIST; /** * A LDAP authority populator that can recursively search static nested groups. * <p>An example of nested groups can be * <pre> * dn: ou=groups,dc=springframework,dc=org * objectClass: top * objectClass: organizationalUnit * ou: groups * * dn: cn=developers,ou=groups,dc=springframework,dc=org * objectClass: groupOfNames * objectClass: top * cn: developers * description: Spring Security Developers * member: uid=ben,ou=people,dc=springframework,dc=org * member: uid=luke,ou=people,dc=springframework,dc=org * member: cn=java-developers,ou=groups,dc=springframework,dc=org * * ou: java-developers * dn: cn=java-developers,ou=groups,dc=springframework,dc=org * objectClass: groupOfNames * objectClass: top * cn: developers * description: Spring Security Java Developers * member: uid=filip,ou=people,dc=springframework,dc=org * ou: java-developer * </pre> * <p> * During an authentication */ public class NestedLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator { public static final String MEMBER_OF = "memberOf"; private static final Log logger = LogFactory.getLog(NestedLdapAuthoritiesPopulator.class); private Set<String> attributeNames; 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); } @Override public Collection<GrantedAuthority> getGrantedAuthorities(DirContextOperations user, String username) { if (MEMBER_OF.equals(getGroupSearchBase())) { String[] memberOfs = user.getStringAttributes(MEMBER_OF); if (memberOfs==null || memberOfs.length==0) { return EMPTY_LIST; } else { return Arrays.stream(memberOfs).map(s -> new LdapAuthority(s,s)).collect(Collectors.toList()); } } else { return super.getGrantedAuthorities(user, username); } } @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; } protected void performNestedSearch(String userDn, String username, Set<GrantedAuthority> authorities, int depth) { if (depth==0) { //back out of recursion 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 (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()) { logRoles(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; 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)); } } } protected void logRoles(Set<Map<String, String[]>> userRoles) { int counter = 0; StringBuffer logDebug = new StringBuffer(); for (Map<String,String[]> debugRoles : userRoles) { for (String debugRoleKey : debugRoles.keySet()) { logDebug.append(++counter); logDebug.append(".["); logDebug.append("Key:"); logDebug.append(debugRoleKey); logDebug.append(" Values:"); for (String debugValues : debugRoles.get(debugRoleKey)) { logDebug.append(debugValues); logDebug.append("; "); } logDebug.append("] "); } } if (counter>0) { logger.debug("Roles from LDAP search:" + logDebug); } else { logger.debug("No Roles from LDAP search returned"); } } public Set<String> getAttributeNames() { return attributeNames; } public void setAttributeNames(Set<String> attributeNames) { this.attributeNames = attributeNames; } public int getMaxSearchDepth() { return maxSearchDepth; } public void setMaxSearchDepth(int maxSearchDepth) { this.maxSearchDepth = maxSearchDepth; } }