SEC-513: First check in of user management stuff.

This commit is contained in:
Luke Taylor 2007-09-07 20:01:46 +00:00
parent 9b71b5aa00
commit 70239a9769
13 changed files with 1142 additions and 77 deletions

View File

@ -0,0 +1,42 @@
package org.acegisecurity.userdetails;
/**
* An extension of the {@link UserDetailsService} which provides the ability
* to create new users and update existing ones.
*
* @author Luke Taylor
* @since 2.0
* @version $Id$
*/
public interface UserDetailsManager extends UserDetailsService {
/**
* Create a new user with the supplied details.
*/
void createUser(UserDetails user);
/**
* Update the specified user.
*/
void updateUser(UserDetails user);
/**
* Remove the user with the given login name from the system.
*/
void deleteUser(String username);
/**
* Modify the current user's password.
*
*
* @param oldPassword current password (for re-authentication if required)
* @param newPassword the password to change to
*/
void changePassword(String oldPassword, String newPassword);
/**
* Check if a user with the supplied login name exists in the system.
*/
boolean userExists(String username);
}

View File

@ -0,0 +1,106 @@
/* 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.acegisecurity.userdetails.ldap;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
/**
* UserDetails implementation whose properties are based on a subset of the
* LDAP schema for <tt>inetOrgPerson</tt>.
*
* <p>
* The username will be mapped from the <tt>uid</tt> attribute by default.
* </p>
*
* @author Luke
* @version $Id$
*/
public class InetOrgPerson extends Person {
private String mail;
private String uid;
private String employeeNumber;
private String destinationIndicator;
public String getMail() {
return mail;
}
public String getUid() {
return uid;
}
public String getEmployeeNumber() {
return employeeNumber;
}
public String getDestinationIndicator() {
return destinationIndicator;
}
protected void populateContext(DirContextAdapter adapter) {
super.populateContext(adapter);
adapter.setAttributeValue("mail", mail);
adapter.setAttributeValue("uid", uid);
adapter.setAttributeValue("employeeNumber", employeeNumber);
adapter.setAttributeValue("destinationIndicator", destinationIndicator);
adapter.setAttributeValues("objectclass", new String[] {"top", "person", "organizationalPerson", "inetOrgPerson"});
}
public static class Essence extends Person.Essence {
public Essence() {
}
public Essence(InetOrgPerson copyMe) {
super(copyMe);
setMail(copyMe.getMail());
setUid(copyMe.getUid());
setDestinationIndicator(copyMe.getDestinationIndicator());
setEmployeeNumber(copyMe.getEmployeeNumber());
}
public Essence(DirContextOperations ctx) {
super(ctx);
setMail(ctx.getStringAttribute("mail"));
setUid(ctx.getStringAttribute("uid"));
setEmployeeNumber(ctx.getStringAttribute("employeeNumber"));
setDestinationIndicator(ctx.getStringAttribute("destinationIndicator"));
}
protected LdapUserDetailsImpl createTarget() {
return new InetOrgPerson();
}
public void setMail(String email) {
((InetOrgPerson) instance).mail = email;
}
public void setUid(String uid) {
((InetOrgPerson) instance).uid = uid;
if(instance.getUsername() == null) {
setUsername(uid);
}
}
public void setEmployeeNumber(String no) {
((InetOrgPerson) instance).employeeNumber = no;
}
public void setDestinationIndicator(String destination) {
((InetOrgPerson) instance).destinationIndicator = destination;
}
}
}

View File

@ -0,0 +1,46 @@
/* 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.acegisecurity.userdetails.ldap;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.GrantedAuthority;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.util.Assert;
/**
* @author Luke Taylor
* @version $Id$
*/
public class InetOrgPersonContextMapper implements UserDetailsContextMapper {
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, GrantedAuthority[] authorities) {
InetOrgPerson.Essence p = new InetOrgPerson.Essence(ctx);
p.setUsername(username);
p.setAuthorities(authorities);
return p.createUserDetails();
}
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
Assert.isInstanceOf(InetOrgPerson.class, user, "UserDetails must be an InetOrgPerson instance");
InetOrgPerson p = (InetOrgPerson) user;
p.populateContext(ctx);
}
}

View File

@ -34,6 +34,7 @@ public interface LdapUserDetails extends UserDetails {
* The attributes for the user's entry in the directory (or a subset of them, depending on what was
* retrieved from the directory)
*
* @deprecated Map additional attributes to properties in a subclass rather than accessing them here.
* @return the user's attributes, or an empty array if none were obtained, never null.
*/
Attributes getAttributes();

View File

@ -18,10 +18,13 @@ package org.acegisecurity.userdetails.ldap;
import org.acegisecurity.GrantedAuthority;
import org.springframework.util.Assert;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Iterator;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
@ -30,10 +33,14 @@ import javax.naming.ldap.Control;
/**
* A UserDetails implementation which is used internally by the Ldap services. It also 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>
* 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>
*
* @author Luke Taylor
* @version $Id$
@ -41,7 +48,6 @@ import javax.naming.ldap.Control;
public class LdapUserDetailsImpl implements LdapUserDetails {
//~ Static fields/initializers =====================================================================================
private static final long serialVersionUID = 1L;
private static final GrantedAuthority[] NO_AUTHORITIES = new GrantedAuthority[0];
private static final Control[] NO_CONTROLS = new Control[0];
@ -110,10 +116,14 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
* Variation of essence pattern. Used to create mutable intermediate object
*/
public static class Essence {
private LdapUserDetailsImpl instance = createTarget();
protected LdapUserDetailsImpl instance = createTarget();
private List mutableAuthorities = new ArrayList();
public Essence() {}
public Essence() { }
public Essence(DirContextOperations ctx) {
setDn(ctx.getDn().toString());
}
public Essence(LdapUserDetails copyMe) {
setDn(copyMe.getDn());
@ -128,14 +138,27 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
setAuthorities(copyMe.getAuthorities());
}
LdapUserDetailsImpl createTarget() {
protected LdapUserDetailsImpl createTarget() {
return new LdapUserDetailsImpl();
}
public Essence addAuthority(GrantedAuthority a) {
mutableAuthorities.add(a);
/** Adds the authority to the list, unless it is already there, in which case it is ignored */
public void addAuthority(GrantedAuthority a) {
if(!hasAuthority(a)) {
mutableAuthorities.add(a);
}
}
return this;
private boolean hasAuthority(GrantedAuthority a) {
Iterator authorities = mutableAuthorities.iterator();
while(authorities.hasNext()) {
GrantedAuthority authority = (GrantedAuthority) authorities.next();
if(authority.equals(a)) {
return true;
}
}
return false;
}
public LdapUserDetails createUserDetails() {
@ -155,62 +178,44 @@ public class LdapUserDetailsImpl implements LdapUserDetails {
return (GrantedAuthority[]) mutableAuthorities.toArray(new GrantedAuthority[0]);
}
public Essence setAccountNonExpired(boolean accountNonExpired) {
public void setAccountNonExpired(boolean accountNonExpired) {
instance.accountNonExpired = accountNonExpired;
return this;
}
public Essence setAccountNonLocked(boolean accountNonLocked) {
public void setAccountNonLocked(boolean accountNonLocked) {
instance.accountNonLocked = accountNonLocked;
return this;
}
public Essence setAttributes(Attributes attributes) {
public void setAttributes(Attributes attributes) {
instance.attributes = attributes;
return this;
}
public Essence setAuthorities(GrantedAuthority[] authorities) {
public void setAuthorities(GrantedAuthority[] authorities) {
mutableAuthorities = new ArrayList(Arrays.asList(authorities));
return this;
}
public void setControls(Control[] controls) {
instance.controls = controls;
}
public Essence setCredentialsNonExpired(boolean credentialsNonExpired) {
public void setCredentialsNonExpired(boolean credentialsNonExpired) {
instance.credentialsNonExpired = credentialsNonExpired;
return this;
}
public Essence setDn(String dn) {
public void setDn(String dn) {
instance.dn = dn;
return this;
}
public Essence setEnabled(boolean enabled) {
public void setEnabled(boolean enabled) {
instance.enabled = enabled;
return this;
}
public Essence setPassword(String password) {
public void setPassword(String password) {
instance.password = password;
return this;
}
public Essence setUsername(String username) {
public void setUsername(String username) {
instance.username = username;
return this;
}
}
}

View File

@ -0,0 +1,414 @@
/* 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.acegisecurity.userdetails.ldap;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.acegisecurity.userdetails.UserDetailsManager;
import org.acegisecurity.ldap.LdapUtils;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.springframework.dao.DataAccessException;
import org.springframework.util.Assert;
import org.springframework.ldap.support.DistinguishedName;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.ldap.LdapTemplate;
import org.springframework.ldap.AttributesMapper;
import org.springframework.ldap.ContextSource;
import org.springframework.ldap.ContextExecutor;
import org.springframework.ldap.SearchExecutor;
import org.springframework.ldap.EntryNotFoundException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.naming.*;
import javax.naming.ldap.LdapContext;
import javax.naming.directory.*;
import java.util.*;
/**
* An Ldap implementation of UserDetailsManager.
* <p>
* It is designed around a standard setup where users and groups/roles are stored under separate contexts,
* defined by the "userDnBase" and "groupSearchBase" properties respectively.
* </p>
* <p>
* In this case, LDAP is being used purely to retrieve information and this class can be used in place of any other
* UserDetailsService for authentication. Authentication isn't performed directly against the directory, unlike with the
* LDAP authentication provider setup.
* </p>
*
*
* @author Luke Taylor
* @since 2.0
*/
public class LdapUserDetailsManager implements UserDetailsManager {
private final Log logger = LogFactory.getLog(LdapUserDetailsManager.class);
/** The DN under which users entries are stored */
private DistinguishedName userDnBase = new DistinguishedName("cn=users");
/** The DN under which groups are stored */
private DistinguishedName groupSearchBase = new DistinguishedName("cn=groups");
/** The attribute which contains the user login name, and which is used by default to build the DN for new users */
private String usernameAttributeName = "uid";
/** Password attribute name */
private String passwordAttributeName = "userPassword";
/** The attribute which corresponds to the role name of a group. */
private String groupRoleAttributeName ="cn";
/** The attribute which contains members of a group */
private String groupMemberAttributeName = "uniquemember";
private String rolePrefix = "ROLE_";
/** The pattern to be used for the user search. {0} is the user's DN */
private String groupSearchFilter = "(uniquemember={0})";
/**
* The strategy used to create a UserDetails object from the LDAP context, username and list of authorities.
* This should be set to match the required UserDetails implementation.
*/
private UserDetailsContextMapper userDetailsMapper = new InetOrgPersonContextMapper();
private LdapTemplate template;
/** Default context mapper used to create a set of roles from a list of attributes */
private AttributesMapper roleMapper = new AttributesMapper() {
public Object mapFromAttributes(Attributes attributes) throws NamingException {
Attribute roleAttr = attributes.get(groupRoleAttributeName);
NamingEnumeration ne = roleAttr.getAll();
// assert ne.hasMore();
Object group = ne.next();
String role = group.toString();
return new GrantedAuthorityImpl(rolePrefix + role.toUpperCase());
}
};
private String[] attributesToRetrieve = null;
public LdapUserDetailsManager(ContextSource contextSource) {
template = new LdapTemplate(contextSource);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
DistinguishedName dn = buildDn(username);
GrantedAuthority[] authorities = getUserAuthorities(dn, username);
logger.debug("Loading user '"+ username + "' with DN '" + dn + "'");
DirContextAdapter userCtx = loadUserAsContext(dn, username);
return userDetailsMapper.mapUserFromContext(userCtx, username, authorities);
}
private UserContext loadUserAsContext(final DistinguishedName dn, final String username) {
return (UserContext) template.executeReadOnly(new ContextExecutor() {
public Object executeWithContext(DirContext ctx) throws NamingException {
try {
Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
return new UserContext(attrs, LdapUtils.getFullDn(dn, ctx));
} catch(NameNotFoundException notFound) {
throw new UsernameNotFoundException("User " + username + " not found", notFound);
}
}
});
}
/**
* Changes the password for the current user. The username is obtained from the security context.
* <p>
* If the old password is supplied, the update will be made by rebinding as the user, thus modifying the password
* using the user's permissions. If <code>oldPassword</code> is null, the update will be attempted using a
* standard read/write context supplied by the context source.
* </p>
*
* @param oldPassword the old password
* @param newPassword the new value of the password.
*/
public void changePassword(final String oldPassword, final String newPassword) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Assert.notNull(authentication,
"No authentication object found in security context. Can't change current user's password!");
String username = authentication.getName();
logger.debug("Changing password for user '"+ username);
final DistinguishedName dn = buildDn(username);
final ModificationItem[] passwordChange = new ModificationItem[] {
new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(passwordAttributeName, newPassword))
};
if(oldPassword == null) {
template.modifyAttributes(dn, passwordChange);
return;
}
template.executeReadWrite(new ContextExecutor() {
public Object executeWithContext(DirContext dirCtx) throws NamingException {
LdapContext ctx = (LdapContext) dirCtx;
ctx.removeFromEnvironment("com.sun.jndi.ldap.connect.pool");
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, LdapUtils.getFullDn(dn, ctx).toUrl());
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, oldPassword);
ctx.reconnect(null);
ctx.modifyAttributes(dn, passwordChange);
return null;
}
});
}
/**
*
* @param dn the distinguished name of the entry - may be either relative to the base context
* or a complete DN including the name of the context (either is supported).
* @param username the user whose roles are required.
* @return the granted authorities returned by the group search
*/
GrantedAuthority[] getUserAuthorities(final DistinguishedName dn, final String username) {
SearchExecutor se = new SearchExecutor() {
public NamingEnumeration executeSearch(DirContext ctx) throws NamingException {
DistinguishedName fullDn = LdapUtils.getFullDn(dn, ctx);
SearchControls ctrls = new SearchControls();
ctrls.setReturningAttributes(new String[] {groupRoleAttributeName});
return ctx.search(groupSearchBase, groupSearchFilter, new String[] {fullDn.toUrl(), username}, ctrls);
}
};
LdapTemplate.AttributesMapperCallbackHandler roleCollector =
template.new AttributesMapperCallbackHandler(roleMapper);
template.search(se, roleCollector);
List authorities = roleCollector.getList();
return (GrantedAuthority[]) authorities.toArray(new GrantedAuthority[authorities.size()]);
}
// protected String getRoleFilter(DistinguishedName dn, String username) {
// return new EqualsFilter("uniquemember", dn.toString()).encode();
// }
public void createUser(UserDetails user) {
DirContextAdapter ctx = new DirContextAdapter();
copyToContext(user, ctx);
DistinguishedName dn = buildDn(user.getUsername());
// Check for any existing authorities which might be set for this DN
GrantedAuthority[] authorities = getUserAuthorities(dn, user.getUsername());
if(authorities.length > 0) {
removeAuthorities(dn, authorities);
}
logger.debug("Creating new user '"+ user.getUsername() + "' with DN '" + dn + "'");
template.bind(dn, ctx, null);
addAuthorities(dn, user.getAuthorities());
}
public void updateUser(UserDetails user) {
// Assert.notNull(attributesToRetrieve, "Configuration must specify a list of attributes in order to use update.");
DistinguishedName dn = buildDn(user.getUsername());
logger.debug("Updating user '"+ user.getUsername() + "' with DN '" + dn + "'");
GrantedAuthority[] authorities = getUserAuthorities(dn, user.getUsername());
UserContext ctx = loadUserAsContext(dn, user.getUsername());
ctx.setUpdateMode(true);
copyToContext(user, ctx);
// Remove the objectclass attribute from the list of mods (if present).
List mods = new LinkedList(Arrays.asList(ctx.getModificationItems()));
ListIterator modIt = mods.listIterator();
while(modIt.hasNext()) {
ModificationItem mod = (ModificationItem) modIt.next();
Attribute a = mod.getAttribute();
if("objectclass".equalsIgnoreCase(a.getID())) {
modIt.remove();
}
}
template.modifyAttributes(dn, (ModificationItem[]) mods.toArray(new ModificationItem[mods.size()]));
// template.rebind(dn, ctx, null);
// Remove the old authorities and replace them with the new one
removeAuthorities(dn, authorities);
addAuthorities(dn, user.getAuthorities());
}
public void deleteUser(String username) {
DistinguishedName dn = buildDn(username);
removeAuthorities(dn, getUserAuthorities(dn, username));
template.unbind(dn);
}
public boolean userExists(String username) {
DistinguishedName dn = buildDn(username);
try {
Object obj = template.lookup(dn);
if (obj instanceof Context) {
LdapUtils.closeContext((Context) obj);
}
return true;
} catch(EntryNotFoundException e) {
return false;
}
}
/**
* Constructs a DN from a username.
* <p>
* The default implementation appends a name component to the <tt>userDnBase</tt> context using the
* <tt>usernameAttributeName</tt> property. So if the <tt>uid</tt> attribute is used to store the username, and the
* base DN is <tt>cn=users</tt> and we are creating a new user called "sam", then the DN will be
* <tt>uid=sam,cn=users</tt>.
*
* @param username the user name used for authentication.
* @return the corresponding DN, relative to the base context.
*/
protected DistinguishedName buildDn(String username) {
DistinguishedName dn = new DistinguishedName(userDnBase);
dn.add(usernameAttributeName, username);
return dn;
}
/**
* Creates a DN from a group name.
*
* @param group the name of the group
* @return the DN of the corresponding group, including the groupSearchBase
*/
protected DistinguishedName buildGroupDn(String group) {
DistinguishedName dn = new DistinguishedName(groupSearchBase);
dn.add(groupRoleAttributeName, group.toLowerCase());
return dn;
}
protected void copyToContext(UserDetails user, DirContextAdapter ctx) {
userDetailsMapper.mapUserToContext(user, ctx);
}
private void addAuthorities(DistinguishedName userDn, GrantedAuthority[] authorities) {
modifyAuthorities(userDn, authorities, DirContext.ADD_ATTRIBUTE);
}
private void removeAuthorities(DistinguishedName userDn, GrantedAuthority[] authorities) {
modifyAuthorities(userDn, authorities, DirContext.REMOVE_ATTRIBUTE);
}
private void modifyAuthorities(final DistinguishedName userDn, final GrantedAuthority[] authorities, final int modType) {
template.executeReadWrite(new ContextExecutor() {
public Object executeWithContext(DirContext ctx) throws NamingException {
for(int i=0; i < authorities.length; i++) {
GrantedAuthority authority = authorities[i];
String group = convertAuthorityToGroup(authority);
DistinguishedName fullDn = LdapUtils.getFullDn(userDn, ctx);
ModificationItem addGroup = new ModificationItem(modType,
new BasicAttribute(groupMemberAttributeName, fullDn.toUrl()));
ctx.modifyAttributes(buildGroupDn(group), new ModificationItem[] {addGroup});
}
return null;
}
});
}
private String convertAuthorityToGroup(GrantedAuthority authority) {
String group = authority.getAuthority();
if(group.startsWith(rolePrefix)) {
group = group.substring(rolePrefix.length());
}
return group;
}
public void setUsernameAttributeName(String usernameAttributeName) {
this.usernameAttributeName = usernameAttributeName;
}
public void setPasswordAttributeName(String passwordAttributeName) {
this.passwordAttributeName = passwordAttributeName;
}
public void setGroupSearchBase(String groupSearchBase) {
this.groupSearchBase = new DistinguishedName(groupSearchBase);
}
public void setGroupRoleAttributeName(String groupRoleAttributeName) {
this.groupRoleAttributeName = groupRoleAttributeName;
}
public void setUserDnBase(String userDnBase) {
this.userDnBase = new DistinguishedName(userDnBase);
}
public void setAttributesToRetrieve(String[] attributesToRetrieve) {
Assert.notNull(attributesToRetrieve);
this.attributesToRetrieve = attributesToRetrieve;
}
public void setUserDetailsMapper(UserDetailsContextMapper userDetailsMapper) {
this.userDetailsMapper = userDetailsMapper;
}
/**
* Sets the name of the multi-valued attribute which holds the DNs of users who are members of a group.
* <p>
* Usually this will be <tt>uniquemember</tt> (the default value) or <tt>member</tt>.
* </p>
*
* @param groupMemberAttributeName the name of the attribute used to store group members.
*/
public void setGroupMemberAttributeName(String groupMemberAttributeName) {
Assert.hasText(groupMemberAttributeName);
this.groupMemberAttributeName = groupMemberAttributeName;
this.groupSearchFilter = "(" + groupMemberAttributeName + "={0})";
}
public void setRoleMapper(AttributesMapper roleMapper) {
this.roleMapper = roleMapper;
}
/**
* This class allows us to set the <tt>updateMode</tt> property of DirContextAdapter when updating existing users.
*/
private static class UserContext extends DirContextAdapter {
public UserContext(Attributes pAttrs, Name dn) {
super(pAttrs, dn);
}
protected void setUpdateMode(boolean mode) {
super.setUpdateMode(mode);
}
}
}

View File

@ -24,6 +24,10 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.ldap.ContextMapper;
import org.springframework.ldap.UncategorizedLdapException;
import org.springframework.ldap.AttributesIntegrityViolationException;
import org.springframework.ldap.support.DirContextAdapter;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
@ -37,10 +41,11 @@ import javax.naming.directory.Attributes;
* @author Luke Taylor
* @version $Id$
*/
public class LdapUserDetailsMapper implements LdapEntryMapper {
public class LdapUserDetailsMapper implements ContextMapper {
//~ Instance fields ================================================================================================
private final Log logger = LogFactory.getLog(LdapUserDetailsMapper.class);
// private String usernameAttributeName = "uid";
private String passwordAttributeName = "userPassword";
private String rolePrefix = "ROLE_";
private String[] roleAttributes = null;
@ -48,42 +53,45 @@ public class LdapUserDetailsMapper implements LdapEntryMapper {
//~ Methods ========================================================================================================
public Object mapAttributes(String dn, Attributes attributes)
throws NamingException {
public Object mapFromContext(Object ctxObj) {
Assert.isInstanceOf(DirContextAdapter.class, ctxObj, "Can only map from DirContextAdapter instances");
DirContextAdapter ctx = (DirContextAdapter)ctxObj;
String dn = ctx.getNameInNamespace();
logger.debug("Mapping user details from context with DN: " + dn);
LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence();
essence.setDn(dn);
essence.setAttributes(attributes);
essence.setAttributes(ctx.getAttributes());
Attribute passwordAttribute = attributes.get(passwordAttributeName);
Attribute passwordAttribute = ctx.getAttributes().get(passwordAttributeName);
if (passwordAttribute != null) {
essence.setPassword(mapPassword(passwordAttribute));
}
// essence.setUsername(mapUsername(ctx));
// Map the roles
for (int i = 0; (roleAttributes != null) && (i < roleAttributes.length); i++) {
Attribute roleAttribute = attributes.get(roleAttributes[i]);
String[] rolesForAttribute = ctx.getStringAttributes(roleAttributes[i]);
if (roleAttribute == null) {
if (rolesForAttribute == null) {
logger.debug("Couldn't read role attribute '" + roleAttributes[i] + "' for user " + dn);
continue;
}
NamingEnumeration attributeRoles = roleAttribute.getAll();
while (attributeRoles.hasMore()) {
GrantedAuthority authority = createAuthority(attributeRoles.next());
for (int j = 0; j < rolesForAttribute.length; j++) {
GrantedAuthority authority = createAuthority(rolesForAttribute[j]);
if (authority != null) {
essence.addAuthority(authority);
} else {
logger.debug("Failed to create an authority value from attribute with Id: "
+ roleAttribute.getID());
}
}
}
//return essence.createUserDetails();
return essence;
}
@ -94,8 +102,14 @@ public class LdapUserDetailsMapper implements LdapEntryMapper {
* @param passwordAttribute the attribute instance containing the password
* @return a String representation of the password.
*/
protected String mapPassword(Attribute passwordAttribute) throws NamingException {
Object retrievedPassword = passwordAttribute.get();
protected String mapPassword(Attribute passwordAttribute) {
Object retrievedPassword = null;
try {
retrievedPassword = passwordAttribute.get();
} catch (NamingException e) {
throw new UncategorizedLdapException("Failed to get password attribute", e);
}
if (!(retrievedPassword instanceof String)) {
// Assume it's binary
@ -106,6 +120,24 @@ public class LdapUserDetailsMapper implements LdapEntryMapper {
}
// protected String mapUsername(DirContextAdapter ctx) {
// Attribute usernameAttribute = ctx.getAttributes().get(usernameAttributeName);
// String username;
//
// if (usernameAttribute == null) {
// throw new AttributesIntegrityViolationException(
// "Failed to get attribute " + usernameAttributeName + " from context");
// }
//
// try {
// username = (String) usernameAttribute.get();
// } catch (NamingException e) {
// throw new UncategorizedLdapException("Failed to get username from attribute " + usernameAttributeName, e);
// }
//
// return username;
// }
/**
* Creates a GrantedAuthority from a role attribute. Override to customize
* authority object creation.
@ -148,10 +180,15 @@ public class LdapUserDetailsMapper implements LdapEntryMapper {
this.passwordAttributeName = passwordAttributeName;
}
// public void setUsernameAttributeName(String usernameAttributeName) {
// this.usernameAttributeName = usernameAttributeName;
// }
/**
* The names of any attributes in the user's entry which represent application
* roles. These will be converted to <tt>GrantedAuthority</tt>s and added to the
* list in the returned LdapUserDetails object.
* list in the returned LdapUserDetails object. The attribute values must be Strings by default.
*
* @param roleAttributes the names of the role attributes.
*/

View File

@ -0,0 +1,113 @@
/* 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.acegisecurity.userdetails.ldap;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.util.Assert;
import org.acegisecurity.ldap.LdapUtils;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
/**
* UserDetails implementation whose properties are based on the LDAP schema for <tt>Person</tt>.
*
* @author Luke
* @since 2.0
* @version $Id$
*/
public class Person extends LdapUserDetailsImpl {
private String sn;
private List cn = new ArrayList();
protected Person() {
}
public String getSn() {
return sn;
}
public String[] getCn() {
return (String[]) cn.toArray(new String[cn.size()]);
}
protected void populateContext(DirContextAdapter adapter) {
adapter.setAttributeValue("sn", sn);
adapter.setAttributeValues("cn", getCn());
if(getPassword() != null) {
adapter.setAttributeValue("userPassword", getPassword());
}
adapter.setAttributeValues("objectclass", new String[] {"top", "person"});
}
public static class Essence extends LdapUserDetailsImpl.Essence {
public Essence() {
}
public Essence(DirContextOperations ctx) {
super(ctx);
setCn(ctx.getStringAttributes("cn"));
setSn(ctx.getStringAttribute("sn"));
Object passo = ctx.getObjectAttribute("userPassword");
if(passo != null) {
String password = LdapUtils.convertPasswordToString(passo);
setPassword(password);
}
}
public Essence(Person copyMe) {
super(copyMe);
setSn(copyMe.sn);
((Person) instance).cn = new ArrayList(copyMe.cn);
}
protected LdapUserDetailsImpl createTarget() {
return new Person();
}
public void setSn(String sn) {
((Person) instance).sn = sn;
}
public void setCn(String[] cn) {
((Person) instance).cn = Arrays.asList(cn);
}
public void addCn(String value) {
((Person) instance).cn.add(value);
}
public LdapUserDetails createUserDetails() {
Person p = (Person) super.createUserDetails();
Assert.hasLength(p.sn);
Assert.notNull(p.cn);
Assert.notEmpty(p.cn);
// TODO: Check contents for null entries
return p;
}
}
}

View File

@ -0,0 +1,31 @@
package org.acegisecurity.userdetails.ldap;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.GrantedAuthority;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.util.Assert;
/**
* @author Luke Taylor
* @version $Id$
*/
public class PersonContextMapper implements UserDetailsContextMapper {
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, GrantedAuthority[] authorities) {
Person.Essence p = new Person.Essence(ctx);
p.setUsername(username);
p.setAuthorities(authorities);
return p.createUserDetails();
}
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
Assert.isInstanceOf(Person.class, user, "UserDetails must be a Person instance");
Person p = (Person) user;
p.populateContext(ctx);
}
}

View File

@ -0,0 +1,47 @@
/* 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.acegisecurity.userdetails.ldap;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.GrantedAuthority;
import org.springframework.ldap.support.DirContextOperations;
import org.springframework.ldap.support.DirContextAdapter;
/**
* Operations to map a UserDetails object to and from a Spring LDAP <tt>DirContextOperations</tt> implementation.
* Used by LdapUserDetailsManager when loading and saving/creating user information.
*
* @author Luke Taylor
* @since 2.0
* @version $Id$
*/
public interface UserDetailsContextMapper {
/**
* Creates a fully populated UserDetails object for use by the security framework.
*
* @param ctx the context object which contains the user information.
* @param username the user's supplied login name.
* @param authority the list of authorities which the user should be given.
* @return the user object.
*/
UserDetails mapUserFromContext(DirContextOperations ctx, String username, GrantedAuthority[] authority);
/**
* Reverse of the above operation. Populates a context object from the supplied user object.
* Called when saving a user, for example.
*/
void mapUserToContext(UserDetails user, DirContextAdapter ctx);
}

View File

@ -0,0 +1,65 @@
package org.acegisecurity.userdetails.ldap;
import junit.framework.TestCase;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.ldap.support.DistinguishedName;
/**
* @author Luke Taylor
* @version $Id$
*/
public class InetOrgPersonTests extends TestCase {
public void testUsernameIsMappedFromContextUidIfNotSet() {
InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
assertEquals("ghengis", p.getUsername());
}
public void testUsernameIsDifferentFromContextUidIfSet() {
InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
essence.setUsername("joe");
InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
assertEquals("joe", p.getUsername());
assertEquals("ghengis", p.getUid());
}
public void testAttributesMapCorrectlyFromContext() {
InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
assertEquals("ghengis@mongolia", p.getMail());
assertEquals("Khan", p.getSn());
assertEquals("Ghengis Khan", p.getCn()[0]);
assertEquals("00001", p.getEmployeeNumber());
assertEquals("West", p.getDestinationIndicator());
}
public void testPasswordIsSetFromContextUserPassword() {
InetOrgPerson.Essence essence = new InetOrgPerson.Essence(createUserContext());
InetOrgPerson p = (InetOrgPerson) essence.createUserDetails();
assertEquals("pillage", p.getPassword());
}
private DirContextAdapter createUserContext() {
DirContextAdapter ctx = new DirContextAdapter();
ctx.setDn(new DistinguishedName("ignored=ignored"));
ctx.setAttributeValue("uid", "ghengis");
ctx.setAttributeValue("userPassword", "pillage");
ctx.setAttributeValue("mail", "ghengis@mongolia");
ctx.setAttributeValue("cn", "Ghengis Khan");
ctx.setAttributeValue("sn", "Khan");
ctx.setAttributeValue("employeeNumber", "00001");
ctx.setAttributeValue("destinationIndicator", "West");
ctx.setAttributeValue("o", "Hordes");
return ctx;
}
}

View File

@ -0,0 +1,153 @@
/* 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.acegisecurity.userdetails.ldap;
import org.acegisecurity.ldap.AbstractLdapServerTestCase;
import org.acegisecurity.ldap.LdapUtils;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.GrantedAuthorityImpl;
import org.springframework.ldap.LdapTemplate;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.ldap.support.DistinguishedName;
import org.springframework.dao.DataAccessException;
import javax.naming.directory.DirContext;
import java.util.List;
import java.util.Iterator;
/**
* @author Luke Taylor
* @version $Id$
*/
public class LdapUserDetailsManagerTests extends AbstractLdapServerTestCase {
private static final GrantedAuthority[] TEST_AUTHORITIES = new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_CLOWNS"),
new GrantedAuthorityImpl("ROLE_ACROBATS")};
private LdapUserDetailsManager mgr;
private LdapTemplate template;
protected void onSetUp() {
mgr = new LdapUserDetailsManager(getInitialCtxFactory());
template = new LdapTemplate(getInitialCtxFactory());
DirContextAdapter ctx = new DirContextAdapter();
ctx.setAttributeValue("objectclass", "organizationalUnit");
ctx.setAttributeValue("ou", "testpeople");
template.bind("ou=testpeople", ctx, null);
ctx.setAttributeValue("ou", "testgroups");
template.bind("ou=testgroups", ctx, null);
DirContextAdapter group = new DirContextAdapter();
group.setAttributeValue("objectclass", "groupOfNames");
group.setAttributeValue("cn", "clowns");
template.bind("cn=clowns,ou=testgroups", ctx, null);
group.setAttributeValue("cn", "acrobats");
template.bind("cn=acrobats,ou=testgroups", ctx, null);
mgr.setUserDnBase("ou=testpeople");
mgr.setGroupSearchBase("ou=testgroups");
mgr.setGroupRoleAttributeName("cn");
mgr.setGroupMemberAttributeName("member");
mgr.setUserDetailsMapper(new PersonContextMapper());
}
protected void tearDown() throws Exception {
Iterator people = template.list("ou=testpeople").iterator();
DirContext rootCtx = new DirContextAdapter(new DistinguishedName(getInitialCtxFactory().getRootDn()));
while(people.hasNext()) {
template.unbind(LdapUtils.getRelativeName((String) people.next(), rootCtx));
}
template.unbind("ou=testpeople");
template.unbind("cn=acrobats,ou=testgroups");
template.unbind("cn=clowns,ou=testgroups");
template.unbind("ou=testgroups");
}
public void testLoadUserByUsernameReturnsCorrectData() {
mgr.setUserDnBase("ou=people");
mgr.setGroupSearchBase("ou=groups");
UserDetails bob = mgr.loadUserByUsername("bob");
assertEquals("bob", bob.getUsername());
// password isn't read
//assertEquals("bobspassword", bob.getPassword());
assertEquals(1, bob.getAuthorities().length);
}
public void testLoadingInvalidUsernameThrowsUsernameNotFoundException() {
try {
mgr.loadUserByUsername("jim");
fail("Expected UsernameNotFoundException for user 'jim'");
} catch(UsernameNotFoundException expected) {
// expected
}
}
public void testUserExistsReturnsTrueForValidUser() {
mgr.setUserDnBase("ou=people");
assertTrue(mgr.userExists("bob"));
}
public void testUserExistsReturnsFalseForInValidUser() {
assertFalse(mgr.userExists("jim"));
}
public void testCreateNewUserSucceeds() {
InetOrgPerson.Essence p = new InetOrgPerson.Essence();
p.setCn(new String[] {"Joe Smeth"});
p.setSn("Smeth");
p.setUid("joe");
p.setAuthorities(TEST_AUTHORITIES);
mgr.createUser(p.createUserDetails());
}
public void testDeleteUserSucceeds() {
InetOrgPerson.Essence p = new InetOrgPerson.Essence();
p.setCn(new String[] {"Don Smeth"});
p.setSn("Smeth");
p.setUid("don");
p.setAuthorities(TEST_AUTHORITIES);
mgr.createUser(p.createUserDetails());
mgr.setUserDetailsMapper(new InetOrgPersonContextMapper());
InetOrgPerson don = (InetOrgPerson) mgr.loadUserByUsername("don");
assertEquals(2, don.getAuthorities().length);
mgr.deleteUser("don");
try {
mgr.loadUserByUsername("don");
fail("Expected UsernameNotFoundException after deleting user");
} catch(UsernameNotFoundException expected) {
// expected
}
// Check that no authorities are left
assertEquals(0, mgr.getUserAuthorities(mgr.buildDn("don"), "don").length);
}
}

View File

@ -21,6 +21,8 @@ import javax.naming.directory.BasicAttributes;
import javax.naming.directory.BasicAttribute;
import org.acegisecurity.GrantedAuthorityImpl;
import org.springframework.ldap.support.DirContextAdapter;
import org.springframework.ldap.support.DistinguishedName;
/**
* Tests {@link LdapUserDetailsMapper}.
@ -38,14 +40,11 @@ public class LdapUserDetailsMapperTests extends TestCase {
mapper.setRoleAttributes(new String[] {"userRole"});
BasicAttributes attrs = new BasicAttributes();
BasicAttribute roleAttribute = new BasicAttribute("userRole");
roleAttribute.add("X");
roleAttribute.add("Y");
roleAttribute.add("Z");
attrs.put(roleAttribute);
DirContextAdapter ctx = new DirContextAdapter();
LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapAttributes("cn=someName", attrs);
ctx.setAttributeValues("userRole", new String[] {"X", "Y", "Z"});
LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapFromContext(ctx);
assertEquals(3, user.getGrantedAuthorities().length);
}
@ -61,24 +60,28 @@ public class LdapUserDetailsMapperTests extends TestCase {
BasicAttributes attrs = new BasicAttributes();
attrs.put(new BasicAttribute("userRole", "x"));
LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapAttributes("cn=someName", attrs);
DirContextAdapter ctx = new DirContextAdapter(attrs, new DistinguishedName("cn=someName"));
LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapFromContext(ctx);
assertEquals(1, user.getGrantedAuthorities().length);
assertEquals("ROLE_X", user.getGrantedAuthorities()[0].getAuthority());
}
public void testNonStringRoleAttributeIsIgnoredByDefault() throws Exception {
LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
mapper.setRoleAttributes(new String[] {"userRole"});
BasicAttributes attrs = new BasicAttributes();
attrs.put(new BasicAttribute("userRole", new GrantedAuthorityImpl("X")));
LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapAttributes("cn=someName", attrs);
assertEquals(0, user.getGrantedAuthorities().length);
}
// public void testNonStringRoleAttributeIsIgnoredByDefault() throws Exception {
// LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
//
// mapper.setRoleAttributes(new String[] {"userRole"});
//
// BasicAttributes attrs = new BasicAttributes();
// attrs.put(new BasicAttribute("userRole", new GrantedAuthorityImpl("X")));
//
// DirContextAdapter ctx = new DirContextAdapter(attrs, new DistinguishedName("cn=someName"));
//
// LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapFromContext(ctx);
//
// assertEquals(0, user.getGrantedAuthorities().length);
// }
public void testPasswordAttributeIsMappedCorrectly() throws Exception {
LdapUserDetailsMapper mapper = new LdapUserDetailsMapper();
@ -87,8 +90,10 @@ public class LdapUserDetailsMapperTests extends TestCase {
BasicAttributes attrs = new BasicAttributes();
attrs.put(new BasicAttribute("myappsPassword", "mypassword".getBytes()));
DirContextAdapter ctx = new DirContextAdapter(attrs, new DistinguishedName("cn=someName"));
LdapUserDetails user =
((LdapUserDetailsImpl.Essence) mapper.mapAttributes("cn=someName", attrs)).createUserDetails();
((LdapUserDetailsImpl.Essence) mapper.mapFromContext(ctx)).createUserDetails();
assertEquals("mypassword", user.getPassword());
}