/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.ldap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.naming.Name;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.AuthenticatedLdapEntryContextCallback;
import org.springframework.ldap.core.AuthenticationErrorCallback;
import org.springframework.ldap.core.ContextExecutor;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapEntryIdentification;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.ldap.SpringSecurityLdapTemplate;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.util.Assert;
/**
* BindingLdapAuthoritiesPopulator: modified DefaultLdapAuthoritiesPopulator
* that binds the user before extracting roles.
*
* Needed for Windows ActiveDirectory support and maybe other LDAP servers
* requiring binding before searches.
*
* @author "Mauro Bartolomeoli - mauro.bartolomeoli@geo-solutions.it"
*
*/
public class BindingLdapAuthoritiesPopulator implements
LdapAuthoritiesPopulator {
// ~ Static fields/initializers
// =====================================================================================
private static final Log logger = LogFactory
.getLog(BindingLdapAuthoritiesPopulator.class);
// ~ Instance fields
// ================================================================================================
/**
* A default role which will be assigned to all authenticated users if set
*/
private GrantedAuthority defaultRole;
private final SpringSecurityLdapTemplate ldapTemplate;
/**
* Controls used to determine whether group searches should be performed
* over the full sub-tree from the base DN. Modified by searchSubTree
* property
*/
private final SearchControls searchControls = new SearchControls();
/**
* The ID of the attribute which contains the role name for a group
*/
private String groupRoleAttribute = "cn";
/**
* The base DN from which the search for group membership should be
* performed
*/
private String groupSearchBase;
/**
* The pattern to be used for the user search. {0} is the user's DN
*/
private String groupSearchFilter = "(member={0})";
private String rolePrefix = "ROLE_";
private boolean convertToUpperCase = true;
// ~ Constructors
// ===================================================================================================
/**
* 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 context factory. If null, no search will be
* performed.
*/
public BindingLdapAuthoritiesPopulator(ContextSource contextSource,
String groupSearchBase) {
Assert.notNull(contextSource, "contextSource must not be null");
// use a binding LdapTemplate, that doesn't make searches without
// authentication
ldapTemplate = new BindingLdapTemplate(contextSource);
ldapTemplate.setSearchControls(searchControls);
this.groupSearchBase = groupSearchBase;
if (groupSearchBase == null) {
logger.info("groupSearchBase is null. No group search will be performed.");
} else if (groupSearchBase.length() == 0) {
logger.info("groupSearchBase is empty. Searches will be performed from the context source base");
}
}
// ~ Methods
// ========================================================================================================
/**
* This method should be overridden if required to obtain any additional
* roles for the given user (on top of those obtained from the standard
* search implemented by this class).
*
* @param user
* the context representing the user who's roles are required
* @return the extra roles which will be merged with those returned by the
* group search
*/
protected Set<GrantedAuthority> getAdditionalRoles(DirContext ctx,
DirContextOperations user, String username) {
return null;
}
/**
* Obtains the authorities for the user who's directory entry is represented
* by the supplied LdapUserDetails object.
*
* @param user
* the user who's authorities are required (or user:password to
* be used to bind to ldap server prior to the search
* operations).
*
* @return the set of roles granted to the user.
*/
public final Collection<GrantedAuthority> getGrantedAuthorities(
final DirContextOperations user, final String username) {
return getGrantedAuthorities(user, username, null);
}
/**
* Obtains the authorities for the user who's directory entry is represented
* by the supplied LdapUserDetails object.
*
* @param user
* the user who's authorities are required
* @param pw be used to bind to ldap server prior to the search
* operations, null otherwise
*
* @return the set of roles granted to the user.
*/
public final Collection<GrantedAuthority> getGrantedAuthorities(
final DirContextOperations user, final String username, final String password) {
final String userDn = user.getNameInNamespace();
if (logger.isDebugEnabled()) {
logger.debug("Getting authorities for user " + userDn);
}
final List<GrantedAuthority> result = new ArrayList<GrantedAuthority>();
// password included -> authenticate before search
if (password != null) {
// authenticate and execute role extraction in the authenticated
// context
ldapTemplate.authenticate(DistinguishedName.EMPTY_PATH, userDn,
password, new AuthenticatedLdapEntryContextCallback() {
@Override
public void executeWithContext(DirContext ctx,
LdapEntryIdentification ldapEntryIdentification) {
getAllRoles(user, userDn, result, username, ctx);
}
});
} else {
getAllRoles(user, userDn, result, username, null);
}
return result;
}
public Set<GrantedAuthority> getGroupMembershipRoles(final DirContext ctx,
String userDn, String username) {
if (getGroupSearchBase() == null) {
return new HashSet<GrantedAuthority>();
}
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
if (logger.isDebugEnabled()) {
logger.debug("Searching for roles for user '" + username
+ "', DN = " + "'" + userDn + "', with filter "
+ groupSearchFilter + " in search base '"
+ getGroupSearchBase() + "'");
}
SpringSecurityLdapTemplate authTemplate;
authTemplate = (SpringSecurityLdapTemplate) LDAPUtils
.getLdapTemplateInContext(ctx, ldapTemplate);
Set<String> userRoles = authTemplate.searchForSingleAttributeValues(
getGroupSearchBase(), groupSearchFilter, new String[] { userDn,
username }, groupRoleAttribute);
if (logger.isDebugEnabled()) {
logger.debug("Roles from search: " + userRoles);
}
for (String role : userRoles) {
if (convertToUpperCase) {
role = role.toUpperCase();
}
authorities.add(new SimpleGrantedAuthority(rolePrefix + role));
}
return authorities;
}
protected ContextSource getContextSource() {
return ldapTemplate.getContextSource();
}
protected String getGroupSearchBase() {
return groupSearchBase;
}
/**
* @deprecated Convert case in the {@code AuthenticationProvider} using a
* {@code GrantedAuthoritiesMapper}.
*/
@Deprecated
public void setConvertToUpperCase(boolean convertToUpperCase) {
this.convertToUpperCase = convertToUpperCase;
}
/**
* The default role which will be assigned to all users.
*
* @param defaultRole
* the role name, including any desired prefix.
* @deprecated Assign a default role in the {@code AuthenticationProvider}
* using a {@code GrantedAuthoritiesMapper}.
*/
@Deprecated
public void setDefaultRole(String defaultRole) {
Assert.notNull(defaultRole,
"The defaultRole property cannot be set to null");
this.defaultRole = new SimpleGrantedAuthority(defaultRole);
}
public void setGroupRoleAttribute(String groupRoleAttribute) {
Assert.notNull(groupRoleAttribute,
"groupRoleAttribute must not be null");
this.groupRoleAttribute = groupRoleAttribute;
}
public void setGroupSearchFilter(String groupSearchFilter) {
Assert.notNull(groupSearchFilter, "groupSearchFilter must not be null");
this.groupSearchFilter = groupSearchFilter;
}
/**
* Sets the prefix which will be prepended to the values loaded from the
* directory. Defaults to "ROLE_" for compatibility with <tt>RoleVoter/tt>.
*
* @deprecated Map the authorities in the {@code AuthenticationProvider}
* using a {@code GrantedAuthoritiesMapper}.
*/
@Deprecated
public void setRolePrefix(String rolePrefix) {
Assert.notNull(rolePrefix, "rolePrefix must not be null");
this.rolePrefix = rolePrefix;
}
/**
* If set to true, a subtree scope search will be performed. If false a
* single-level search is used.
*
* @param searchSubtree
* set to true to enable searching of the entire tree below the
* <tt>groupSearchBase</tt>.
*/
public void setSearchSubtree(boolean searchSubtree) {
int searchScope = searchSubtree ? SearchControls.SUBTREE_SCOPE
: SearchControls.ONELEVEL_SCOPE;
searchControls.setSearchScope(searchScope);
}
/**
* Sets the corresponding property on the underlying template, avoiding
* specific issues with Active Directory.
*
* @see LdapTemplate#setIgnoreNameNotFoundException(boolean)
*/
public void setIgnorePartialResultException(boolean ignore) {
ldapTemplate.setIgnorePartialResultException(ignore);
}
private void getAllRoles(final DirContextOperations user,
final String userDn, final List<GrantedAuthority> result,
final String userName, DirContext ctx) {
Set<GrantedAuthority> roles = getGroupMembershipRoles(ctx, userDn,
userName);
Set<GrantedAuthority> extraRoles = getAdditionalRoles(ctx, user,
userName);
if (extraRoles != null) {
roles.addAll(extraRoles);
}
if (defaultRole != null) {
roles.add(defaultRole);
}
result.addAll(roles);
}
}