mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-26 13:53:14 +00:00
SEC-264: changes to LDAP services.
This commit is contained in:
parent
db042046e9
commit
65fe641900
@ -0,0 +1,31 @@
|
||||
/* Copyright 2004, 2005 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.acegisecurity.ldap;
|
||||
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.NamingException;
|
||||
|
||||
/**
|
||||
* A mapper for use with {@link LdapTemplate}. Creates a customized object from
|
||||
* a set of attributes retrieved from a directory entry.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public interface LdapEntryMapper {
|
||||
|
||||
public Object mapAttributes(String dn, Attributes attributes) throws NamingException;
|
||||
}
|
@ -25,7 +25,10 @@ import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.Attribute;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
@ -45,37 +48,66 @@ public class LdapTemplate {
|
||||
public static final String[] NO_ATTRS = new String[0];
|
||||
|
||||
private InitialDirContextFactory dirContextFactory;
|
||||
private String managerDn = null;
|
||||
private String principalDn = null;
|
||||
private String password = null;
|
||||
/** Default search scope */
|
||||
private int searchScope = SearchControls.SUBTREE_SCOPE;
|
||||
/** Default search controls */
|
||||
private SearchControls searchControls = new SearchControls();
|
||||
|
||||
public LdapTemplate(InitialDirContextFactory dirContextFactory) {
|
||||
Assert.notNull(dirContextFactory, "An InitialDirContextFactory is required");
|
||||
this.dirContextFactory = dirContextFactory;
|
||||
|
||||
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dirContextFactory the source of DirContexts
|
||||
* @param userDn the user name to authenticate as when obtaining new contexts
|
||||
* @param password the user's password
|
||||
*/
|
||||
public LdapTemplate(InitialDirContextFactory dirContextFactory, String userDn, String password) {
|
||||
this(dirContextFactory);
|
||||
|
||||
Assert.hasLength(userDn, "managerDn must not be null or empty");
|
||||
Assert.hasLength(userDn, "userDn must not be null or empty");
|
||||
Assert.notNull(password, "password cannot be null");
|
||||
|
||||
this.managerDn = userDn;
|
||||
this.principalDn = userDn;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public void setSearchScope(int searchScope) {
|
||||
this.searchScope = searchScope;
|
||||
searchControls.setSearchScope(searchScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* The time (in milliseconds) which to wait before the search fails;
|
||||
* the default is zero, meaning forever.
|
||||
*/
|
||||
public void setSearchTimeLimit(int searchTimeLimit) {
|
||||
searchControls.setTimeLimit(searchTimeLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the corresponding property on the SearchControls instance used
|
||||
* in the search.
|
||||
*
|
||||
*/
|
||||
public void setDerefLinkFlag(boolean deref) {
|
||||
searchControls.setDerefLinkFlag(deref);
|
||||
}
|
||||
|
||||
public void setSearchControls(SearchControls searchControls) {
|
||||
this.searchControls = searchControls;
|
||||
}
|
||||
|
||||
public Object execute(LdapCallback callback) throws DataAccessException {
|
||||
DirContext ctx = null;
|
||||
|
||||
try {
|
||||
ctx = (managerDn == null) ?
|
||||
ctx = (principalDn == null) ?
|
||||
dirContextFactory.newInitialDirContext() :
|
||||
dirContextFactory.newInitialDirContext(managerDn, password);
|
||||
dirContextFactory.newInitialDirContext(principalDn, password);
|
||||
|
||||
return callback.execute(ctx);
|
||||
|
||||
@ -98,8 +130,10 @@ public class LdapTemplate {
|
||||
ctls.setReturningAttributes(NO_ATTRS);
|
||||
ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
|
||||
|
||||
String relativeName = LdapUtils.getRelativeName(dn, ctx);
|
||||
|
||||
NamingEnumeration results =
|
||||
ctx.search(dn, comparisonFilter, new Object[]{value}, ctls);
|
||||
ctx.search(relativeName, comparisonFilter, new Object[]{value}, ctls);
|
||||
|
||||
return Boolean.valueOf(results.hasMore());
|
||||
}
|
||||
@ -132,7 +166,9 @@ public class LdapTemplate {
|
||||
|
||||
SearchControls ctls = new SearchControls();
|
||||
|
||||
ctls.setSearchScope(searchScope);
|
||||
ctls.setSearchScope(searchControls.getSearchScope());
|
||||
ctls.setTimeLimit(searchControls.getTimeLimit());
|
||||
ctls.setDerefLinkFlag(searchControls.getDerefLinkFlag());
|
||||
ctls.setReturningAttributes(new String[] {attributeName});
|
||||
|
||||
NamingEnumeration matchingEntries =
|
||||
@ -191,13 +227,65 @@ public class LdapTemplate {
|
||||
* @param attributesToRetrieve the named attributes which will be retrieved from the directory entry.
|
||||
* @return the object created by the mapper
|
||||
*/
|
||||
public Object retrieveEntry(final String dn, final AttributesMapper mapper, final String[] attributesToRetrieve) {
|
||||
public Object retrieveEntry(final String dn, final LdapEntryMapper mapper, final String[] attributesToRetrieve) {
|
||||
return execute ( new LdapCallback() {
|
||||
|
||||
public Object execute(DirContext ctx) throws NamingException {
|
||||
return mapper.mapAttributes( ctx.getAttributes(LdapUtils.getRelativeName(dn, ctx), attributesToRetrieve) );
|
||||
return mapper.mapAttributes(dn, ctx.getAttributes(LdapUtils.getRelativeName(dn, ctx), attributesToRetrieve) );
|
||||
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param base
|
||||
* @param filter
|
||||
* @param params
|
||||
* @param mapper
|
||||
* @return the object created by the mapper from the matching entry
|
||||
* @throws EmptyResultDataAccessException if no results are found.
|
||||
* @throws IncorrectResultSizeDataAccessException if the search returns more than one result.
|
||||
*/
|
||||
public Object searchForSingleEntry(final String base, final String filter, final Object[] params, final LdapEntryMapper mapper) {
|
||||
return execute ( new LdapCallback() {
|
||||
|
||||
public Object execute(DirContext ctx) throws NamingException {
|
||||
NamingEnumeration results = ctx.search(base, filter, params, searchControls);
|
||||
|
||||
if (!results.hasMore()) {
|
||||
throw new EmptyResultDataAccessException(1);
|
||||
}
|
||||
|
||||
SearchResult searchResult = (SearchResult)results.next();
|
||||
|
||||
if (results.hasMore()) {
|
||||
throw new IncorrectResultSizeDataAccessException(1);
|
||||
}
|
||||
|
||||
// Work out the DN of the matched entry
|
||||
StringBuffer dn = new StringBuffer(searchResult.getName());
|
||||
|
||||
if (base.length() > 0) {
|
||||
dn.append(",");
|
||||
dn.append(base);
|
||||
}
|
||||
|
||||
String nameInNamespace = ctx.getNameInNamespace();
|
||||
|
||||
if(StringUtils.hasLength(nameInNamespace)) {
|
||||
dn.append(",");
|
||||
dn.append(nameInNamespace);
|
||||
}
|
||||
|
||||
return mapper.mapAttributes(dn.toString(), searchResult.getAttributes());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,16 +22,9 @@ import javax.naming.NamingException;
|
||||
/**
|
||||
* A user representation which is used internally by the Ldap provider.
|
||||
*
|
||||
* It contains the user's distinguished name and a set of attributes that
|
||||
* have been retrieved from the Ldap server.
|
||||
* <p>
|
||||
* An instance may be created as the result of a search, or when user information
|
||||
* is retrieved during authentication.
|
||||
* </p>
|
||||
* <p>
|
||||
* An instance of this class will be used by the <tt>LdapAuthenticationProvider</tt>
|
||||
* to construct the final user details object that it returns.
|
||||
* </p>
|
||||
*
|
||||
* @deprecated in favour of {@link org.acegisecurity.userdetails.ldap.LdapUserDetails}
|
||||
*
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
|
@ -15,6 +15,8 @@
|
||||
|
||||
package org.acegisecurity.ldap;
|
||||
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
/**
|
||||
* Obtains a user's information from the LDAP directory given a login name.
|
||||
* <p>
|
||||
@ -33,9 +35,8 @@ public interface LdapUserSearch {
|
||||
* for that user.
|
||||
*
|
||||
* @param username the login name supplied to the authentication service.
|
||||
* @return an LdapUserInfo object containing the user's full DN and requested attributes.
|
||||
* TODO: Need to optionally supply required attributes here for the search.
|
||||
* @return an LdapUserDetailsImpl object containing the user's full DN and requested attributes.
|
||||
*/
|
||||
LdapUserInfo searchForUser(String username);
|
||||
LdapUserDetails searchForUser(String username);
|
||||
|
||||
}
|
||||
|
@ -16,24 +16,22 @@
|
||||
package org.acegisecurity.ldap.search;
|
||||
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsMapper;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.acegisecurity.ldap.LdapUserSearch;
|
||||
import org.acegisecurity.ldap.LdapUtils;
|
||||
import org.acegisecurity.ldap.InitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.LdapDataAccessException;
|
||||
import org.acegisecurity.ldap.LdapTemplate;
|
||||
import org.acegisecurity.ldap.LdapEntryMapper;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.NamingEnumeration;
|
||||
|
||||
/**
|
||||
* LdapUserSearch implementation which uses an Ldap filter to locate the user.
|
||||
@ -82,6 +80,8 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
|
||||
|
||||
private InitialDirContextFactory initialDirContextFactory;
|
||||
|
||||
private LdapEntryMapper userDetailsMapper = new LdapUserDetailsMapper();
|
||||
|
||||
//~ Methods ================================================================
|
||||
|
||||
public FilterBasedLdapUserSearch(String searchBase,
|
||||
@ -104,12 +104,12 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
|
||||
//~ Methods ================================================================
|
||||
|
||||
/**
|
||||
* Return the LdapUserInfo containing the user's information, or null if
|
||||
* no SearchResult is found.
|
||||
* Return the LdapUserDetailsImpl containing the user's information
|
||||
*
|
||||
* @param username the username to search for.
|
||||
* @throws UsernameNotFoundException if no matching entry is found.
|
||||
*/
|
||||
public LdapUserInfo searchForUser(String username) {
|
||||
public LdapUserDetails searchForUser(String username) {
|
||||
DirContext ctx = initialDirContextFactory.newInitialDirContext();
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
@ -117,42 +117,20 @@ public class FilterBasedLdapUserSearch implements LdapUserSearch {
|
||||
", with user search " + this.toString());
|
||||
}
|
||||
|
||||
LdapTemplate template = new LdapTemplate(initialDirContextFactory);
|
||||
|
||||
template.setSearchControls(searchControls);
|
||||
|
||||
try {
|
||||
String[] args = new String[] { LdapUtils.escapeNameForFilter(username) };
|
||||
Object user = template.searchForSingleEntry(searchBase, searchFilter, new String[] { username }, userDetailsMapper);
|
||||
Assert.isInstanceOf(LdapUserDetails.class, user, "Entry mapper must return an LdapUserDetailsImpl instance");
|
||||
|
||||
NamingEnumeration results = ctx.search(searchBase, searchFilter, args, searchControls);
|
||||
return (LdapUserDetails)user;
|
||||
|
||||
if (!results.hasMore()) {
|
||||
throw new UsernameNotFoundException("User " + username + " not found in directory.");
|
||||
}
|
||||
|
||||
SearchResult searchResult = (SearchResult)results.next();
|
||||
|
||||
if (results.hasMore()) {
|
||||
throw new BadCredentialsException("Expected a single user but search returned multiple results");
|
||||
}
|
||||
|
||||
StringBuffer userDn = new StringBuffer(searchResult.getName());
|
||||
|
||||
if (searchBase.length() > 0) {
|
||||
userDn.append(",");
|
||||
userDn.append(searchBase);
|
||||
}
|
||||
|
||||
String nameInNamespace = ctx.getNameInNamespace();
|
||||
|
||||
if(StringUtils.hasLength(nameInNamespace)) {
|
||||
userDn.append(",");
|
||||
userDn.append(nameInNamespace);
|
||||
}
|
||||
|
||||
return new LdapUserInfo(userDn.toString(), searchResult.getAttributes());
|
||||
|
||||
} catch(NamingException ne) {
|
||||
throw new LdapDataAccessException("User Couldn't be found due to exception", ne);
|
||||
} finally {
|
||||
LdapUtils.closeContext(ctx);
|
||||
} catch(EmptyResultDataAccessException notFound) {
|
||||
throw new UsernameNotFoundException("User " + username + " not found in directory.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,9 +17,9 @@ package org.acegisecurity.providers.ldap;
|
||||
|
||||
import org.acegisecurity.providers.dao.AbstractUserDetailsAuthenticationProvider;
|
||||
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.userdetails.UserDetails;
|
||||
import org.acegisecurity.userdetails.User;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.acegisecurity.AuthenticationException;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
|
||||
@ -29,8 +29,6 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.naming.directory.Attributes;
|
||||
|
||||
/**
|
||||
* An {@link org.acegisecurity.providers.AuthenticationProvider} implementation that
|
||||
* provides integration with an LDAP server.
|
||||
@ -156,33 +154,35 @@ public class LdapAuthenticationProvider extends AbstractUserDetailsAuthenticatio
|
||||
String password = (String)authentication.getCredentials();
|
||||
Assert.notNull(password, "Null password was supplied in authentication token");
|
||||
|
||||
LdapUserInfo ldapUser = authenticator.authenticate(username, password);
|
||||
LdapUserDetails ldapUser = authenticator.authenticate(username, password);
|
||||
|
||||
return createUserDetails(username, password, ldapUser.getDn(), ldapUser.getAttributes());
|
||||
return createUserDetails(ldapUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the user final <tt>UserDetails</tt> object that will be returned by the provider
|
||||
* Creates the final <tt>UserDetails</tt> object that will be returned by the provider
|
||||
* once the user has been authenticated.
|
||||
* <p>
|
||||
* The <tt>LdapAuthoritiesPopulator</tt> will be used to create the granted authorites for the
|
||||
* user.
|
||||
* </p>
|
||||
* <p>
|
||||
* Can be overridden to customize the mapping of user attributes to additional user information.
|
||||
* Can be overridden to customize the creation of the final UserDetails instance. The
|
||||
* default will merge any additional authorities retrieved from the populator with the
|
||||
* original <tt>ldapUser</tt> object.
|
||||
* </p>
|
||||
*
|
||||
* @param username The user login, as passed to the provider
|
||||
* @param password The submitted password
|
||||
* @param userDn The DN of the user in the Ldap system.
|
||||
* @param attributes The user attributes retrieved from the Ldap system.
|
||||
* @param ldapUser The intermediate LdapUserDetails instance returned from the authenticator.
|
||||
*
|
||||
* @return The UserDetails for the successfully authenticated user.
|
||||
*/
|
||||
protected UserDetails createUserDetails(String username, String password, String userDn, Attributes attributes) {
|
||||
protected UserDetails createUserDetails(LdapUserDetails ldapUser) {
|
||||
|
||||
return new User(username, password, true, true, true, true,
|
||||
authoritiesPopulator.getGrantedAuthorities(username, userDn, attributes));
|
||||
LdapUserDetailsImpl.Essence user = new LdapUserDetailsImpl.Essence(ldapUser);
|
||||
|
||||
user.setAuthorities(authoritiesPopulator.getGrantedAuthorities(ldapUser));
|
||||
|
||||
return user.createUserDetails();
|
||||
}
|
||||
|
||||
protected LdapAuthoritiesPopulator getAuthoritiesPoulator() {
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
package org.acegisecurity.providers.ldap;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
/**
|
||||
* The strategy interface for locating and authenticating an Ldap user.
|
||||
@ -37,5 +37,5 @@ public interface LdapAuthenticator {
|
||||
* @param password the user's password supplied at login.
|
||||
* @return the details of the successfully authenticated user.
|
||||
*/
|
||||
LdapUserInfo authenticate(String username, String password);
|
||||
LdapUserDetails authenticate(String username, String password);
|
||||
}
|
||||
|
@ -16,10 +16,9 @@
|
||||
package org.acegisecurity.providers.ldap;
|
||||
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.acegisecurity.ldap.LdapDataAccessException;
|
||||
|
||||
import javax.naming.directory.Attributes;
|
||||
|
||||
/**
|
||||
* Obtains a list of granted authorities for an Ldap user.
|
||||
* <p>
|
||||
@ -35,13 +34,11 @@ public interface LdapAuthoritiesPopulator {
|
||||
/**
|
||||
* Get the list of authorities for the user.
|
||||
*
|
||||
* @param username the login name which was passed to the LDAP provider.
|
||||
* @param userDn the full DN of the user
|
||||
* @param userAttributes the user's LDAP attributes that were retrieved from the directory.
|
||||
* @param userDetails the user details object which was returned by the LDAP authenticator.
|
||||
* @return the granted authorities for the given user.
|
||||
* @throws org.acegisecurity.ldap.LdapDataAccessException if there is a problem accessing the directory.
|
||||
*/
|
||||
GrantedAuthority[] getGrantedAuthorities(String username, String userDn, Attributes userAttributes)
|
||||
GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetails)
|
||||
throws LdapDataAccessException;
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ package org.acegisecurity.providers.ldap.authenticator;
|
||||
import org.acegisecurity.providers.ldap.LdapAuthenticator;
|
||||
import org.acegisecurity.ldap.InitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapUserSearch;
|
||||
import org.acegisecurity.ldap.LdapEntryMapper;
|
||||
import org.acegisecurity.AcegiMessageSource;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsMapper;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.Assert;
|
||||
@ -55,6 +57,8 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator,
|
||||
/** The attributes which will be retrieved from the directory. Null means all attributes */
|
||||
private String[] userAttributes = null;
|
||||
|
||||
private LdapEntryMapper userDetailsMapper = new LdapUserDetailsMapper();
|
||||
|
||||
/**
|
||||
* The suffix to be added to the DN patterns, worked out internally from the root DN of the
|
||||
* configured InitialDirContextFactory.
|
||||
@ -137,6 +141,15 @@ public abstract class AbstractLdapAuthenticator implements LdapAuthenticator,
|
||||
this.userSearch = userSearch;
|
||||
}
|
||||
|
||||
public void setUserDetailsMapper(LdapEntryMapper userDetailsMapper) {
|
||||
Assert.notNull("userDetailsMapper must not be null");
|
||||
this.userDetailsMapper = userDetailsMapper;
|
||||
}
|
||||
|
||||
protected LdapEntryMapper getUserDetailsMapper() {
|
||||
return userDetailsMapper;
|
||||
}
|
||||
|
||||
protected LdapUserSearch getUserSearch() {
|
||||
return userSearch;
|
||||
}
|
||||
|
@ -15,18 +15,14 @@
|
||||
|
||||
package org.acegisecurity.providers.ldap.authenticator;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUtils;
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.LdapDataAccessException;
|
||||
import org.acegisecurity.ldap.InitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapTemplate;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.NamingException;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
@ -44,6 +40,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(BindAuthenticator.class);
|
||||
|
||||
|
||||
//~ Constructors ===========================================================
|
||||
|
||||
public BindAuthenticator(InitialDirContextFactory initialDirContextFactory) {
|
||||
@ -52,9 +49,9 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
|
||||
|
||||
//~ Methods ================================================================
|
||||
|
||||
public LdapUserInfo authenticate(String username, String password) {
|
||||
public LdapUserDetails authenticate(String username, String password) {
|
||||
|
||||
LdapUserInfo user = null;
|
||||
LdapUserDetails user = null;
|
||||
|
||||
// If DN patterns are configured, try authenticating with them directly
|
||||
Iterator dns = getUserDns(username).iterator();
|
||||
@ -66,7 +63,7 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
|
||||
// Otherwise use the configured locator to find the user
|
||||
// and authenticate with the returned DN.
|
||||
if (user == null && getUserSearch() != null) {
|
||||
LdapUserInfo userFromSearch = getUserSearch().searchForUser(username);
|
||||
LdapUserDetails userFromSearch = getUserSearch().searchForUser(username);
|
||||
user = bindWithDn(userFromSearch.getDn(), password);
|
||||
}
|
||||
|
||||
@ -80,18 +77,19 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
|
||||
|
||||
}
|
||||
|
||||
LdapUserInfo bindWithDn(String userDn, String password) {
|
||||
DirContext ctx = null;
|
||||
LdapUserInfo user = null;
|
||||
LdapUserDetails bindWithDn(String userDn, String password) {
|
||||
LdapTemplate template = new LdapTemplate(getInitialDirContextFactory(), userDn, password);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Attempting to bind with DN = " + userDn);
|
||||
}
|
||||
|
||||
try {
|
||||
ctx = getInitialDirContextFactory().newInitialDirContext(userDn, password);
|
||||
Attributes attributes = loadAttributes(ctx, userDn);
|
||||
user = new LdapUserInfo(userDn, attributes);
|
||||
|
||||
Object user = (LdapUserDetails)template.retrieveEntry(userDn, getUserDetailsMapper(), getUserAttributes());
|
||||
Assert.isInstanceOf(LdapUserDetails.class, user, "Entry mapper must return an LdapUserDetails instance");
|
||||
|
||||
return (LdapUserDetails) user;
|
||||
|
||||
} catch(BadCredentialsException e) {
|
||||
// This will be thrown if an invalid user name is used and the method may
|
||||
@ -99,24 +97,8 @@ public class BindAuthenticator extends AbstractLdapAuthenticator {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to bind as " + userDn + ": " + e.getCause());
|
||||
}
|
||||
} finally {
|
||||
LdapUtils.closeContext(ctx);
|
||||
}
|
||||
|
||||
return user;
|
||||
return null;
|
||||
}
|
||||
|
||||
Attributes loadAttributes(DirContext ctx, String userDn) {
|
||||
try {
|
||||
return ctx.getAttributes(
|
||||
LdapUtils.getRelativeName(userDn, ctx),
|
||||
getUserAttributes());
|
||||
|
||||
} catch(NamingException ne) {
|
||||
throw new LdapDataAccessException(messages.getMessage(
|
||||
"BindAuthenticator.failedToLoadAttributes", new String[] {userDn},
|
||||
"Failed to load attributes for user {0}"), ne);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,11 +15,10 @@
|
||||
|
||||
package org.acegisecurity.providers.ldap.authenticator;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.LdapUtils;
|
||||
import org.acegisecurity.ldap.InitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapTemplate;
|
||||
import org.acegisecurity.ldap.AttributesMapper;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.acegisecurity.providers.encoding.PasswordEncoder;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
@ -29,13 +28,6 @@ import org.springframework.util.Assert;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.Attributes;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
@ -62,14 +54,10 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
|
||||
|
||||
private static final Log logger = LogFactory.getLog(PasswordComparisonAuthenticator.class);
|
||||
|
||||
private static final String[] NO_ATTRS = new String[0];
|
||||
|
||||
//~ Instance fields ========================================================
|
||||
|
||||
private String passwordAttributeName = "userPassword";
|
||||
|
||||
private String passwordCompareFilter = "(userPassword={0})";
|
||||
|
||||
private PasswordEncoder passwordEncoder = new LdapShaPasswordEncoder();
|
||||
|
||||
//~ Constructors ===========================================================
|
||||
@ -80,10 +68,10 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
|
||||
|
||||
//~ Methods ================================================================
|
||||
|
||||
public LdapUserInfo authenticate(String username, String password) {
|
||||
public LdapUserDetails authenticate(final String username, final String password) {
|
||||
|
||||
// locate the user and check the password
|
||||
LdapUserInfo user = null;
|
||||
LdapUserDetails user = null;
|
||||
|
||||
Iterator dns = getUserDns(username).iterator();
|
||||
|
||||
@ -93,13 +81,7 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
|
||||
final String userDn = (String)dns.next();
|
||||
|
||||
if(ldapTemplate.nameExists(userDn)) {
|
||||
AttributesMapper mapper = new AttributesMapper() {
|
||||
public Object mapAttributes(Attributes attributes) {
|
||||
return new LdapUserInfo(userDn, attributes);
|
||||
}
|
||||
};
|
||||
|
||||
user = (LdapUserInfo)ldapTemplate.retrieveEntry(userDn, mapper, getUserAttributes());
|
||||
user = (LdapUserDetails)ldapTemplate.retrieveEntry(userDn, getUserDetailsMapper(), getUserAttributes());
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,40 +93,36 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
|
||||
throw new UsernameNotFoundException(username);
|
||||
}
|
||||
|
||||
DirContext ctx = getInitialDirContextFactory().newInitialDirContext();
|
||||
String retrievedPassword = user.getPassword();
|
||||
|
||||
try {
|
||||
Attribute passwordAttribute = user.getAttributes().get(passwordAttributeName);
|
||||
|
||||
if(passwordAttribute != null) {
|
||||
Object retrievedPassword = passwordAttribute.get();
|
||||
|
||||
if (!(retrievedPassword instanceof String)) {
|
||||
// Assume it's binary
|
||||
retrievedPassword = new String((byte[])retrievedPassword);
|
||||
}
|
||||
|
||||
if (!verifyPassword(password, (String)retrievedPassword)) {
|
||||
throw new BadCredentialsException(messages.getMessage(
|
||||
"PasswordComparisonAuthenticator.badCredentials",
|
||||
"Bad credentials"));
|
||||
}
|
||||
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Password attribute " + passwordAttributeName
|
||||
+ " wasn't retrieved for user " + username);
|
||||
}
|
||||
|
||||
doPasswordCompare(ctx, user.getRelativeName(ctx), password);
|
||||
if(retrievedPassword != null) {
|
||||
if (!verifyPassword(password, retrievedPassword)) {
|
||||
throw new BadCredentialsException(messages.getMessage(
|
||||
"PasswordComparisonAuthenticator.badCredentials",
|
||||
"Bad credentials"));
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch(NamingException ne) {
|
||||
throw new BadCredentialsException("Authentication failed due to exception ", ne);
|
||||
} finally {
|
||||
LdapUtils.closeContext(ctx);
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Password attribute wasn't retrieved for user '" + username
|
||||
+ "' using mapper " + getUserDetailsMapper()
|
||||
+ ". Performing LDAP compare of password attribute '"
|
||||
+ passwordAttributeName + "'" );
|
||||
}
|
||||
|
||||
String encodedPassword = passwordEncoder.encodePassword(password, null);
|
||||
byte[] passwordBytes = LdapUtils.getUtf8Bytes(encodedPassword);
|
||||
|
||||
if(!ldapTemplate.compare(user.getDn(), passwordAttributeName, passwordBytes)) {
|
||||
|
||||
throw new BadCredentialsException(messages.getMessage(
|
||||
"PasswordComparisonAuthenticator.badCredentials",
|
||||
"Bad credentials"));
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -162,32 +140,9 @@ public final class PasswordComparisonAuthenticator extends AbstractLdapAuthentic
|
||||
return false;
|
||||
}
|
||||
|
||||
private void doPasswordCompare(DirContext ctx, String name, String password) throws NamingException {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Performing LDAP compare of password for " + name);
|
||||
}
|
||||
|
||||
password = passwordEncoder.encodePassword(password, null);
|
||||
byte[] passwordBytes = LdapUtils.getUtf8Bytes(password);
|
||||
|
||||
SearchControls ctls = new SearchControls();
|
||||
ctls.setReturningAttributes(NO_ATTRS);
|
||||
ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
|
||||
|
||||
NamingEnumeration results = ctx.search(name, passwordCompareFilter,
|
||||
new Object[]{passwordBytes}, ctls);
|
||||
|
||||
if(!results.hasMore()) {
|
||||
throw new BadCredentialsException(messages.getMessage(
|
||||
"PasswordComparisonAuthenticator.badCredentials",
|
||||
"Bad credentials"));
|
||||
}
|
||||
}
|
||||
|
||||
public void setPasswordAttributeName(String passwordAttribute) {
|
||||
Assert.hasLength(passwordAttribute, "passwordAttributeName must not be empty or null");
|
||||
this.passwordAttributeName = passwordAttribute;
|
||||
this.passwordCompareFilter = "(" + passwordAttributeName + "={0})";
|
||||
}
|
||||
|
||||
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
|
||||
|
@ -16,24 +16,20 @@
|
||||
package org.acegisecurity.providers.ldap.populator;
|
||||
|
||||
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
|
||||
import org.acegisecurity.ldap.LdapDataAccessException;
|
||||
import org.acegisecurity.ldap.InitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapUtils;
|
||||
import org.acegisecurity.ldap.LdapTemplate;
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.GrantedAuthorityImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.Attribute;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
import javax.naming.directory.DirContext;
|
||||
import javax.naming.NamingEnumeration;
|
||||
import javax.naming.NamingException;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* The default strategy for obtaining user role information from the directory.
|
||||
@ -114,7 +110,7 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
//~ Instance fields ========================================================
|
||||
|
||||
/** Attributes of the User's LDAP Object that contain role name information. */
|
||||
private String[] userRoleAttributes = null;
|
||||
// private String[] userRoleAttributes = null;
|
||||
|
||||
private String rolePrefix = "ROLE_";
|
||||
|
||||
@ -175,21 +171,26 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
//~ Methods ================================================================
|
||||
|
||||
/**
|
||||
*
|
||||
* @param username the login name passed to the authentication provider.
|
||||
* @param userDn the user's DN.
|
||||
* @param userAttributes the attributes retrieved from the user's directory entry.
|
||||
* @return the full set of roles granted to the user.
|
||||
* @return the set of roles granted to the user.
|
||||
*/
|
||||
public GrantedAuthority[] getGrantedAuthorities(String username, String userDn, Attributes userAttributes) {
|
||||
public final GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetails) {
|
||||
String userDn = userDetails.getDn();
|
||||
|
||||
logger.debug("Getting authorities for user " + userDn);
|
||||
|
||||
Set roles = getRolesFromUserAttributes(userDn, userAttributes);
|
||||
Set roles = getGroupMembershipRoles(userDn);
|
||||
|
||||
Set groupRoles = getGroupMembershipRoles(userDn, userAttributes);
|
||||
// Temporary use of deprecated method
|
||||
Set oldGroupRoles = getGroupMembershipRoles(userDn, userDetails.getAttributes());
|
||||
|
||||
if(groupRoles != null) {
|
||||
roles.addAll(groupRoles);
|
||||
if(oldGroupRoles != null) {
|
||||
roles.addAll(oldGroupRoles);
|
||||
}
|
||||
|
||||
Set extraRoles = getAdditionalRoles(userDetails);
|
||||
|
||||
if(extraRoles != null) {
|
||||
roles.addAll(extraRoles);
|
||||
}
|
||||
|
||||
if(defaultRole != null) {
|
||||
@ -199,31 +200,23 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
return (GrantedAuthority[])roles.toArray(new GrantedAuthority[roles.size()]);
|
||||
}
|
||||
|
||||
protected Set getRolesFromUserAttributes(String userDn, Attributes userAttributes) {
|
||||
Set userRoles = new HashSet();
|
||||
// protected Set getRolesFromUserAttributes(String userDn, Attributes userAttributes) {
|
||||
// Set userRoles = new HashSet();
|
||||
//
|
||||
// for(int i=0; userRoleAttributes != null && i < userRoleAttributes.length; i++) {
|
||||
// Attribute roleAttribute = userAttributes.get(userRoleAttributes[i]);
|
||||
//
|
||||
// addAttributeValuesToRoleSet(roleAttribute, userRoles);
|
||||
// }
|
||||
//
|
||||
// return userRoles;
|
||||
// }
|
||||
|
||||
for(int i=0; userRoleAttributes != null && i < userRoleAttributes.length; i++) {
|
||||
Attribute roleAttribute = userAttributes.get(userRoleAttributes[i]);
|
||||
|
||||
addAttributeValuesToRoleSet(roleAttribute, userRoles);
|
||||
}
|
||||
|
||||
return userRoles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for groups the user is a member of.
|
||||
*
|
||||
* @param userDn the user's distinguished name.
|
||||
* @param userAttributes the retrieved user's attributes (unused by default).
|
||||
* @return the set of roles obtained from a group membership search, or null if
|
||||
* <tt>groupSearchBase</tt> has been set.
|
||||
*/
|
||||
protected Set getGroupMembershipRoles(String userDn, Attributes userAttributes) {
|
||||
Set userRoles = new HashSet();
|
||||
private Set getGroupMembershipRoles(String userDn) {
|
||||
Set authorities = new HashSet();
|
||||
|
||||
if (groupSearchBase == null) {
|
||||
return null;
|
||||
return authorities;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
@ -232,82 +225,60 @@ public class DefaultLdapAuthoritiesPopulator implements LdapAuthoritiesPopulator
|
||||
+ " in search base '" + groupSearchBase + "'");
|
||||
}
|
||||
|
||||
DirContext ctx = initialDirContextFactory.newInitialDirContext();
|
||||
SearchControls ctls = new SearchControls();
|
||||
LdapTemplate template = new LdapTemplate(initialDirContextFactory);
|
||||
|
||||
ctls.setSearchScope(searchScope);
|
||||
ctls.setReturningAttributes(new String[] {groupRoleAttribute});
|
||||
template.setSearchScope(searchScope);
|
||||
|
||||
try {
|
||||
NamingEnumeration groups =
|
||||
ctx.search(groupSearchBase, groupSearchFilter, new String[]{userDn}, ctls);
|
||||
|
||||
while (groups.hasMore()) {
|
||||
SearchResult result = (SearchResult) groups.next();
|
||||
Attributes attrs = result.getAttributes();
|
||||
|
||||
// There should only be one role attribute.
|
||||
NamingEnumeration groupRoleAttributes = attrs.getAll();
|
||||
|
||||
while(groupRoleAttributes.hasMore()) {
|
||||
Attribute roleAttribute = (Attribute) groupRoleAttributes.next();
|
||||
|
||||
addAttributeValuesToRoleSet(roleAttribute, userRoles);
|
||||
}
|
||||
}
|
||||
} catch (NamingException e) {
|
||||
throw new LdapDataAccessException("Group search failed for user " + userDn, e);
|
||||
} finally {
|
||||
LdapUtils.closeContext(ctx);
|
||||
}
|
||||
Set userRoles = template.searchForSingleAttributeValues(groupSearchBase, groupSearchFilter, new String[]{userDn}, groupRoleAttribute);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Roles from search: " + userRoles);
|
||||
}
|
||||
|
||||
return userRoles;
|
||||
Iterator it = userRoles.iterator();
|
||||
|
||||
while(it.hasNext()) {
|
||||
Object role = it.next();
|
||||
|
||||
// We only handle Strings for the time being
|
||||
if(role instanceof String) {
|
||||
if(convertToUpperCase) {
|
||||
role = ((String)role).toUpperCase();
|
||||
}
|
||||
|
||||
authorities.add(new GrantedAuthorityImpl(rolePrefix + role));
|
||||
} else {
|
||||
logger.warn("Non-String value found for role: " + role);
|
||||
}
|
||||
}
|
||||
|
||||
return authorities;
|
||||
|
||||
}
|
||||
|
||||
private void addAttributeValuesToRoleSet(Attribute roleAttribute, Set roles) {
|
||||
if (roleAttribute == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
NamingEnumeration attributeRoles = roleAttribute.getAll();
|
||||
protected Set getAdditionalRoles(LdapUserDetails ldapUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while(attributeRoles.hasMore()) {
|
||||
Object role = attributeRoles.next();
|
||||
|
||||
// We only handle Strings for the time being
|
||||
if(role instanceof String) {
|
||||
if(convertToUpperCase) {
|
||||
role = ((String)role).toUpperCase();
|
||||
}
|
||||
|
||||
roles.add(new GrantedAuthorityImpl(rolePrefix + role));
|
||||
} else {
|
||||
logger.warn("Non-String value found for role attribute " + roleAttribute.getID());
|
||||
}
|
||||
}
|
||||
} catch(NamingException ne) {
|
||||
throw new LdapDataAccessException("Error retrieving values for role attribute " +
|
||||
roleAttribute.getID(), ne);
|
||||
}
|
||||
/**
|
||||
* Searches for groups the user is a member of.
|
||||
*
|
||||
* @deprecated Subclasses should implement <tt>getAdditionalRoles</tt> instead.
|
||||
*
|
||||
* @param userDn the user's distinguished name.
|
||||
* @param userAttributes the retrieved user's attributes (unused by default).
|
||||
* @return the set of roles obtained from a group membership search, or null if
|
||||
* <tt>groupSearchBase</tt> has been set.
|
||||
*/
|
||||
protected Set getGroupMembershipRoles(String userDn, Attributes userAttributes) {
|
||||
return new HashSet();
|
||||
}
|
||||
|
||||
protected InitialDirContextFactory getInitialDirContextFactory() {
|
||||
return initialDirContextFactory;
|
||||
}
|
||||
|
||||
protected String[] getUserRoleAttributes() {
|
||||
return userRoleAttributes;
|
||||
}
|
||||
|
||||
public void setUserRoleAttributes(String[] userRoleAttributes) {
|
||||
this.userRoleAttributes = userRoleAttributes;
|
||||
}
|
||||
|
||||
public void setRolePrefix(String rolePrefix) {
|
||||
Assert.notNull(rolePrefix, "rolePrefix must not be null");
|
||||
this.rolePrefix = rolePrefix;
|
||||
|
@ -163,13 +163,26 @@ public class FilterChainProxy implements Filter, InitializingBean,
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
} else {
|
||||
Filter[] filters = obtainAllDefinedFilters(cad);
|
||||
|
||||
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi,
|
||||
filters);
|
||||
virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
Filter[] filters = obtainAllDefinedFilters(cad);
|
||||
|
||||
if(filters.length == 0) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(fi.getRequestUrl() + " has an empty filter list");
|
||||
}
|
||||
|
||||
chain.doFilter(request, response);
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi,
|
||||
filters);
|
||||
virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
|
||||
}
|
||||
|
||||
public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() {
|
||||
@ -224,7 +237,7 @@ public class FilterChainProxy implements Filter, InitializingBean,
|
||||
}
|
||||
}
|
||||
|
||||
return (Filter[]) list.toArray(new Filter[] {null});
|
||||
return (Filter[]) list.toArray(new Filter[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -247,9 +260,10 @@ public class FilterChainProxy implements Filter, InitializingBean,
|
||||
ConfigAttribute attr = (ConfigAttribute) attributes.next();
|
||||
String filterName = attr.getAttribute();
|
||||
|
||||
Assert.notNull(filterName,
|
||||
"Configuration attribute: '" + attr
|
||||
if(filterName == null) {
|
||||
throw new IllegalArgumentException("Configuration attribute: '" + attr
|
||||
+ "' returned null to the getAttribute() method, which is invalid when used with FilterChainProxy");
|
||||
}
|
||||
|
||||
if (!filterName.equals(TOKEN_NONE)) {
|
||||
list.add(this.applicationContext.getBean(filterName,
|
||||
@ -257,7 +271,7 @@ public class FilterChainProxy implements Filter, InitializingBean,
|
||||
}
|
||||
}
|
||||
|
||||
return (Filter[]) list.toArray(new Filter[] {null});
|
||||
return (Filter[]) list.toArray(new Filter[list.size()]);
|
||||
}
|
||||
|
||||
public void setApplicationContext(ApplicationContext applicationContext)
|
||||
|
@ -19,8 +19,6 @@ import junit.framework.TestCase;
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
import org.apache.directory.server.core.jndi.CoreContextFactory;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
@ -31,16 +29,16 @@ public abstract class AbstractLdapServerTestCase extends TestCase {
|
||||
protected static final String MANAGER_PASSWORD = "acegisecurity";
|
||||
|
||||
// External server config
|
||||
// private static final String PROVIDER_URL = "ldap://monkeymachine:389/"+ROOT_DN;
|
||||
// private static final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
|
||||
// private static final Hashtable EXTRA_ENV = new Hashtable();
|
||||
private static final String PROVIDER_URL = "ldap://monkeymachine:389/"+ROOT_DN;
|
||||
private static final String CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
|
||||
private static final Hashtable EXTRA_ENV = new Hashtable();
|
||||
|
||||
|
||||
// Embedded (non-networked) server config
|
||||
private static final LdapTestServer SERVER = new LdapTestServer();
|
||||
private static final String PROVIDER_URL = ROOT_DN;
|
||||
private static final String CONTEXT_FACTORY = CoreContextFactory.class.getName();
|
||||
private static final Hashtable EXTRA_ENV = SERVER.getConfiguration().toJndiEnvironment();
|
||||
// private static final LdapTestServer SERVER = new LdapTestServer();
|
||||
// private static final String PROVIDER_URL = ROOT_DN;
|
||||
// private static final String CONTEXT_FACTORY = CoreContextFactory.class.getName();
|
||||
// private static final Hashtable EXTRA_ENV = SERVER.getConfiguration().toJndiEnvironment();
|
||||
|
||||
protected AbstractLdapServerTestCase() {
|
||||
}
|
||||
|
@ -33,27 +33,27 @@ public class LdapTemplateTests extends AbstractLdapServerTestCase {
|
||||
|
||||
|
||||
public void testCompareOfCorrectValueSucceeds() {
|
||||
assertTrue(template.compare("uid=bob,ou=people", "uid", "bob"));
|
||||
assertTrue(template.compare("uid=bob,ou=people,dc=acegisecurity,dc=org", "uid", "bob"));
|
||||
}
|
||||
|
||||
public void testCompareOfWrongValueFails() {
|
||||
assertFalse(template.compare("uid=bob,ou=people", "uid", "wrongvalue"));
|
||||
assertFalse(template.compare("uid=bob,ou=people,dc=acegisecurity,dc=org", "uid", "wrongvalue"));
|
||||
}
|
||||
|
||||
public void testCompareOfCorrectByteValueSucceeds() {
|
||||
|
||||
// Doesn't work with embedded server due to bugs in apacheds
|
||||
// assertTrue(template.compare("uid=bob,ou=people", "userPassword", LdapUtils.getUtf8Bytes("bobspassword")));
|
||||
// assertTrue(template.compare("uid=bob,ou=people,dc=acegisecurity,dc=org", "userPassword", LdapUtils.getUtf8Bytes("bobspassword")));
|
||||
}
|
||||
|
||||
public void testCompareOfWrongByteValueFails() {
|
||||
|
||||
// Doesn't work with embedded server due to bugs in apacheds
|
||||
// assertFalse(template.compare("uid=bob,ou=people", "userPassword", LdapUtils.getUtf8Bytes("wrongvalue")));
|
||||
// assertFalse(template.compare("uid=bob,ou=people,dc=acegisecurity,dc=org", "userPassword", LdapUtils.getUtf8Bytes("wrongvalue")));
|
||||
}
|
||||
|
||||
public void testSearchForSingleAttributeValues() {
|
||||
String param = "uid=ben,ou=people," + getInitialCtxFactory().getRootDn();
|
||||
String param = "uid=ben,ou=people,dc=acegisecurity,dc=org";
|
||||
|
||||
Set values = template.searchForSingleAttributeValues("ou=groups", "(member={0})", new String[] {param}, "ou");
|
||||
|
||||
|
@ -2,9 +2,9 @@ package org.acegisecurity.ldap.search;
|
||||
|
||||
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
|
||||
import org.acegisecurity.ldap.DefaultInitialDirContextFactory;
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
|
||||
/**
|
||||
* Tests for FilterBasedLdapUserSearch.
|
||||
@ -32,7 +32,7 @@ public class FilterBasedLdapUserSearchTests extends AbstractLdapServerTestCase {
|
||||
public void testBasicSearch() throws Exception {
|
||||
FilterBasedLdapUserSearch locator =
|
||||
new FilterBasedLdapUserSearch("ou=people", "(uid={0})", dirCtxFactory);
|
||||
LdapUserInfo bob = locator.searchForUser("bob");
|
||||
LdapUserDetails bob = locator.searchForUser("bob");
|
||||
locator.setSearchSubtree(false);
|
||||
locator.setSearchTimeLimit(0);
|
||||
// name is wrong with embedded apacheDS
|
||||
@ -45,7 +45,7 @@ public class FilterBasedLdapUserSearchTests extends AbstractLdapServerTestCase {
|
||||
new FilterBasedLdapUserSearch("", "(cn={0})", dirCtxFactory);
|
||||
locator.setSearchSubtree(true);
|
||||
|
||||
LdapUserInfo ben = locator.searchForUser("Ben Alex");
|
||||
LdapUserDetails ben = locator.searchForUser("Ben Alex");
|
||||
// assertEquals("uid=ben,ou=people,"+ROOT_DN, bob.getDn());
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ public class FilterBasedLdapUserSearchTests extends AbstractLdapServerTestCase {
|
||||
try {
|
||||
locator.searchForUser("Ignored");
|
||||
fail("Expected exception for multiple search matches.");
|
||||
} catch (BadCredentialsException expected) {
|
||||
} catch (IncorrectResultSizeDataAccessException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ public class FilterBasedLdapUserSearchTests extends AbstractLdapServerTestCase {
|
||||
dirCtxFactory);
|
||||
|
||||
// Search for bob, get back ben...
|
||||
LdapUserInfo ben = locator.searchForUser("bob");
|
||||
LdapUserDetails ben = locator.searchForUser("bob");
|
||||
String cn = (String)ben.getAttributes().get("cn").get();
|
||||
assertEquals("Ben Alex", cn);
|
||||
// assertEquals("uid=ben,ou=people,"+ROOT_DN, ben.getDn());
|
||||
|
@ -6,11 +6,12 @@ import javax.naming.directory.BasicAttributes;
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.GrantedAuthorityImpl;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
|
||||
import org.acegisecurity.ldap.*;
|
||||
import org.acegisecurity.ldap.DefaultInitialDirContextFactory;
|
||||
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
||||
import org.acegisecurity.userdetails.UserDetails;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
@ -82,18 +83,20 @@ public class LdapAuthenticationProviderTests extends AbstractLdapServerTestCase
|
||||
*/
|
||||
class MockAuthoritiesPopulator implements LdapAuthoritiesPopulator {
|
||||
|
||||
public GrantedAuthority[] getGrantedAuthorities(String userDn, String dn, Attributes userAttributes) {
|
||||
return new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_USER") };
|
||||
public GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetailsll) {
|
||||
return new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_USER") };
|
||||
}
|
||||
}
|
||||
|
||||
class MockAuthenticator implements LdapAuthenticator {
|
||||
Attributes userAttributes = new BasicAttributes("cn","bob");
|
||||
|
||||
public LdapUserInfo authenticate(String username, String password) {
|
||||
public LdapUserDetails authenticate(String username, String password) {
|
||||
if(username.equals("bob") && password.equals("bobspassword")) {
|
||||
|
||||
return new LdapUserInfo("cn=bob,ou=people,dc=acegisecurity,dc=org", userAttributes);
|
||||
LdapUserDetailsImpl.Essence creator = new LdapUserDetailsImpl.Essence();
|
||||
creator.setDn("cn=bob,ou=people,dc=acegisecurity,dc=org");
|
||||
creator.setAttributes(userAttributes);
|
||||
return creator.createUserDetails();
|
||||
}
|
||||
throw new BadCredentialsException("Authentication of Bob failed.");
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
package org.acegisecurity.providers.ldap.authenticator;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.GrantedAuthorityImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsMapper;
|
||||
|
||||
/**
|
||||
* Tests for {@link BindAuthenticator}.
|
||||
@ -26,12 +29,10 @@ public class BindAuthenticatorTests extends AbstractLdapServerTestCase {
|
||||
|
||||
public void testAuthenticationWithCorrectPasswordSucceeds() throws Exception {
|
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=people"});
|
||||
LdapUserInfo user = authenticator.authenticate("bob","bobspassword");
|
||||
LdapUserDetails user = authenticator.authenticate("bob","bobspassword");
|
||||
}
|
||||
|
||||
public void testAuthenticationWithWrongPasswordFails() {
|
||||
// BindAuthenticator authenticator = new BindAuthenticator(dirCtxFactory);
|
||||
|
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=people"});
|
||||
|
||||
try {
|
||||
@ -42,25 +43,36 @@ public class BindAuthenticatorTests extends AbstractLdapServerTestCase {
|
||||
}
|
||||
|
||||
public void testAuthenticationWithUserSearch() throws Exception {
|
||||
LdapUserInfo user = new LdapUserInfo("uid=bob,ou=people," + getInitialCtxFactory().getRootDn(), null);
|
||||
authenticator.setUserSearch(new MockUserSearch(user));
|
||||
LdapUserDetailsImpl.Essence userEssence = new LdapUserDetailsImpl.Essence();
|
||||
userEssence.setDn("uid=bob,ou=people,dc=acegisecurity,dc=org");
|
||||
|
||||
authenticator.setUserSearch(new MockUserSearch(userEssence.createUserDetails()));
|
||||
authenticator.afterPropertiesSet();
|
||||
authenticator.authenticate("bob","bobspassword");
|
||||
}
|
||||
|
||||
public void testAuthenticationWithInvalidUserNameFails() {
|
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=people"});
|
||||
|
||||
// Apache DS falls apart with unknown DNs.
|
||||
//
|
||||
// public void testAuthenticationWithInvalidUserNameFails() {
|
||||
// BindAuthenticator authenticator = new BindAuthenticator();
|
||||
//
|
||||
// authenticator.setInitialDirContextFactory(dirCtxFactory);
|
||||
// authenticator.setUserDnPatterns("cn={0},ou=people");
|
||||
// try {
|
||||
// authenticator.authenticate("Baz","bobspassword");
|
||||
// fail("Shouldn't be able to bind with invalid username");
|
||||
// } catch(BadCredentialsException expected) {
|
||||
// }
|
||||
// }
|
||||
try {
|
||||
authenticator.authenticate("nonexistentsuser","bobspassword");
|
||||
fail("Shouldn't be able to bind with invalid username");
|
||||
} catch(BadCredentialsException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Create separate tests for base class
|
||||
public void testRoleRetrieval() {
|
||||
authenticator.setUserDnPatterns(new String[] {"uid={0},ou=people"});
|
||||
LdapUserDetailsMapper userMapper = new LdapUserDetailsMapper();
|
||||
userMapper.setRoleAttributes(new String[] {"uid"});
|
||||
|
||||
authenticator.setUserDetailsMapper(userMapper);
|
||||
|
||||
LdapUserDetails user = authenticator.authenticate("bob","bobspassword");
|
||||
|
||||
assertEquals(1, user.getAuthorities().length);
|
||||
assertEquals(new GrantedAuthorityImpl("ROLE_BOB"), user.getAuthorities()[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,20 @@
|
||||
package org.acegisecurity.providers.ldap.authenticator;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.LdapUserSearch;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
/**
|
||||
* @author Luke Taylor
|
||||
* @version $Id$
|
||||
*/
|
||||
public class MockUserSearch implements LdapUserSearch {
|
||||
LdapUserInfo user;
|
||||
LdapUserDetails user;
|
||||
|
||||
public MockUserSearch(LdapUserInfo user) {
|
||||
public MockUserSearch(LdapUserDetails user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public LdapUserInfo searchForUser(String username) {
|
||||
public LdapUserDetails searchForUser(String username) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
@ -27,12 +27,13 @@ public class PasswordComparisonAuthenticatorMockTests extends MockObjectTestCase
|
||||
|
||||
// Get the mock to return an empty attribute set
|
||||
mockCtx.expects(atLeastOnce()).method("getNameInNamespace").will(returnValue("dc=acegisecurity,dc=org"));
|
||||
mockCtx.expects(once()).method("lookup").with(eq("cn=Bob,ou=people")).will(returnValue(true));
|
||||
mockCtx.expects(once()).method("getAttributes").with(eq("cn=Bob,ou=people"), NULL).will(returnValue(new BasicAttributes()));
|
||||
// Setup a single return value (i.e. success)
|
||||
Attributes searchResults = new BasicAttributes("", null);
|
||||
mockCtx.expects(once()).method("search").with(eq("cn=Bob,ou=people"),
|
||||
eq("(userPassword={0})"), NOT_NULL, NOT_NULL).will(returnValue(searchResults.getAll()));
|
||||
mockCtx.expects(once()).method("close");
|
||||
mockCtx.expects(atLeastOnce()).method("close");
|
||||
authenticator.authenticate("Bob", "bobspassword");
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package org.acegisecurity.providers.ldap.authenticator;
|
||||
|
||||
import org.acegisecurity.ldap.LdapUserInfo;
|
||||
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
|
||||
import org.acegisecurity.BadCredentialsException;
|
||||
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
||||
|
||||
import javax.naming.directory.BasicAttributes;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsMapper;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
||||
|
||||
/**
|
||||
* Tests for {@link PasswordComparisonAuthenticator}.
|
||||
@ -87,7 +87,7 @@ public class PasswordComparisonAuthenticatorTests extends AbstractLdapServerTest
|
||||
}
|
||||
|
||||
public void testAllAttributesAreRetrivedByDefault() {
|
||||
LdapUserInfo user = authenticator.authenticate("Bob", "bobspassword");
|
||||
LdapUserDetails user = authenticator.authenticate("Bob", "bobspassword");
|
||||
System.out.println(user.getAttributes().toString());
|
||||
assertEquals("User should have 5 attributes", 5, user.getAttributes().size());
|
||||
|
||||
@ -103,7 +103,10 @@ public class PasswordComparisonAuthenticatorTests extends AbstractLdapServerTest
|
||||
}
|
||||
*/
|
||||
public void testUseOfDifferentPasswordAttribute() {
|
||||
LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
|
||||
mapper.setPasswordAttributeName("uid");
|
||||
authenticator.setPasswordAttributeName("uid");
|
||||
authenticator.setUserDetailsMapper(mapper);
|
||||
authenticator.authenticate("bob", "bob");
|
||||
}
|
||||
/*
|
||||
@ -119,10 +122,11 @@ public class PasswordComparisonAuthenticatorTests extends AbstractLdapServerTest
|
||||
authenticator = new PasswordComparisonAuthenticator(getInitialCtxFactory());
|
||||
assertTrue("User DN matches shouldn't be available",
|
||||
authenticator.getUserDns("Bob").isEmpty());
|
||||
LdapUserInfo user = new LdapUserInfo("uid=Bob,ou=people" +
|
||||
getInitialCtxFactory().getRootDn(),
|
||||
new BasicAttributes("userPassword","bobspassword"));
|
||||
authenticator.setUserSearch(new MockUserSearch(user));
|
||||
LdapUserDetailsImpl.Essence userEssence = new LdapUserDetailsImpl.Essence();
|
||||
userEssence.setDn("uid=Bob,ou=people,dc=acegisecurity,dc=org");
|
||||
userEssence.setPassword("bobspassword");
|
||||
|
||||
authenticator.setUserSearch(new MockUserSearch(userEssence.createUserDetails()));
|
||||
authenticator.authenticate("ShouldntBeUsed","bobspassword");
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
package org.acegisecurity.providers.ldap.populator;
|
||||
|
||||
import javax.naming.directory.Attributes;
|
||||
import javax.naming.directory.BasicAttributes;
|
||||
import javax.naming.directory.BasicAttribute;
|
||||
|
||||
import org.acegisecurity.GrantedAuthority;
|
||||
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
||||
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
|
||||
|
||||
import java.util.Set;
|
||||
@ -21,30 +20,39 @@ public class DefaultLdapAuthoritiesPopulatorTests extends AbstractLdapServerTest
|
||||
getInitialCtxFactory().setManagerPassword(MANAGER_PASSWORD);
|
||||
}
|
||||
|
||||
public void testUserAttributeMappingToRoles() {
|
||||
DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator();
|
||||
populator.setUserRoleAttributes(new String[] {"userRole", "otherUserRole"});
|
||||
populator.getUserRoleAttributes();
|
||||
|
||||
Attributes userAttrs = new BasicAttributes();
|
||||
BasicAttribute attr = new BasicAttribute("userRole", "role1");
|
||||
attr.add("role2");
|
||||
userAttrs.put(attr);
|
||||
attr = new BasicAttribute("otherUserRole", "role3");
|
||||
attr.add("role2"); // duplicate
|
||||
userAttrs.put(attr);
|
||||
|
||||
GrantedAuthority[] authorities =
|
||||
populator.getGrantedAuthorities("Ignored", "Ignored", userAttrs);
|
||||
assertEquals("User should have three roles", 3, authorities.length);
|
||||
}
|
||||
// public void testUserAttributeMappingToRoles() {
|
||||
// DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator();
|
||||
// populator.setUserRoleAttributes(new String[] {"userRole", "otherUserRole"});
|
||||
// populator.getUserRoleAttributes();
|
||||
//
|
||||
// Attributes userAttrs = new BasicAttributes();
|
||||
// BasicAttribute attr = new BasicAttribute("userRole", "role1");
|
||||
// attr.add("role2");
|
||||
// userAttrs.put(attr);
|
||||
// attr = new BasicAttribute("otherUserRole", "role3");
|
||||
// attr.add("role2"); // duplicate
|
||||
// userAttrs.put(attr);
|
||||
//
|
||||
// LdapUserDetailsImpl.Essence user = new LdapUserDetailsImpl.Essence();
|
||||
// user.setDn("Ignored");
|
||||
// user.setUsername("Ignored");
|
||||
// user.setAttributes(userAttrs);
|
||||
//
|
||||
// GrantedAuthority[] authorities =
|
||||
// populator.getGrantedAuthorities(user.createUserDetails());
|
||||
// assertEquals("User should have three roles", 3, authorities.length);
|
||||
// }
|
||||
|
||||
public void testDefaultRoleIsAssignedWhenSet() {
|
||||
DefaultLdapAuthoritiesPopulator populator = new DefaultLdapAuthoritiesPopulator();
|
||||
populator.setDefaultRole("ROLE_USER");
|
||||
LdapUserDetailsImpl.Essence user = new LdapUserDetailsImpl.Essence();
|
||||
user.setDn("Ignored");
|
||||
user.setUsername("Ignored");
|
||||
user.setAttributes(new BasicAttributes());
|
||||
|
||||
GrantedAuthority[] authorities =
|
||||
populator.getGrantedAuthorities("Ignored", "Ignored", new BasicAttributes());
|
||||
populator.getGrantedAuthorities(user.createUserDetails());
|
||||
assertEquals(1, authorities.length);
|
||||
assertEquals("ROLE_USER", authorities[0].getAuthority());
|
||||
}
|
||||
@ -59,9 +67,14 @@ public class DefaultLdapAuthoritiesPopulatorTests extends AbstractLdapServerTest
|
||||
populator.setConvertToUpperCase(true);
|
||||
populator.setGroupSearchFilter("(member={0})");
|
||||
|
||||
LdapUserDetailsImpl.Essence user = new LdapUserDetailsImpl.Essence();
|
||||
user.setUsername("ben");
|
||||
user.setDn("uid=ben,ou=people,dc=acegisecurity,dc=org");
|
||||
user.setAttributes(new BasicAttributes());
|
||||
|
||||
GrantedAuthority[] authorities =
|
||||
populator.getGrantedAuthorities("ben", "uid=ben,ou=people,"+
|
||||
getInitialCtxFactory().getRootDn(), new BasicAttributes());
|
||||
populator.getGrantedAuthorities(user.createUserDetails());
|
||||
|
||||
assertEquals("Should have 2 roles", 2, authorities.length);
|
||||
Set roles = new HashSet();
|
||||
roles.add(authorities[0].toString());
|
||||
|
Loading…
x
Reference in New Issue
Block a user