فهرست منبع

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

Luke Taylor 18 سال پیش
والد
کامیت
70239a9769

+ 42 - 0
core/src/main/java/org/acegisecurity/userdetails/UserDetailsManager.java

@@ -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);
+
+}

+ 106 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/InetOrgPerson.java

@@ -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;
+        }
+    }
+}

+ 46 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/InetOrgPersonContextMapper.java

@@ -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);
+    }
+}

+ 1 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/LdapUserDetails.java

@@ -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();

+ 43 - 38
core/src/main/java/org/acegisecurity/userdetails/ldap/LdapUserDetailsImpl.java

@@ -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);
+            }
+        }
+
+        private boolean hasAuthority(GrantedAuthority a) {
+            Iterator authorities = mutableAuthorities.iterator();
 
-            return this;
+            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;
         }
     }
 }

+ 414 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/LdapUserDetailsManager.java

@@ -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);
+        }
+    }
+}

+ 55 - 18
core/src/main/java/org/acegisecurity/userdetails/ldap/LdapUserDetailsMapper.java

@@ -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 {
-        LdapUserDetailsImpl.Essence essence = new LdapUserDetailsImpl.Essence();
+    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.
      */

+ 113 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/Person.java

@@ -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;
+        }
+    }
+}
+
+
+
+
+
+
+

+ 31 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/PersonContextMapper.java

@@ -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);
+    }
+}

+ 47 - 0
core/src/main/java/org/acegisecurity/userdetails/ldap/UserDetailsContextMapper.java

@@ -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);
+}

+ 65 - 0
core/src/test/java/org/acegisecurity/userdetails/ldap/InetOrgPersonTests.java

@@ -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;
+    }
+
+}

+ 153 - 0
core/src/test/java/org/acegisecurity/userdetails/ldap/LdapUserDetailsManagerTests.java

@@ -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);
+    }
+}

+ 26 - 21
core/src/test/java/org/acegisecurity/userdetails/ldap/LdapUserDetailsMapperTests.java

@@ -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();
+
+        ctx.setAttributeValues("userRole", new String[] {"X", "Y", "Z"});
 
-        LdapUserDetailsImpl.Essence user = (LdapUserDetailsImpl.Essence) mapper.mapAttributes("cn=someName", attrs);
+        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());
     }