diff --git a/projects/cadcAccessControl-Server/build.xml b/projects/cadcAccessControl-Server/build.xml
index c4e48687ff5f6e922ab5fa54e6d58b319efff02b..12e688469aee1444d7027bef89f048af9de33f88 100644
--- a/projects/cadcAccessControl-Server/build.xml
+++ b/projects/cadcAccessControl-Server/build.xml
@@ -138,4 +138,20 @@
             value="${ext.lib}/commons-logging.jar"/>
   <property name="testingJars"
             value="${lib.commons-logging}:${dev.junit}:${dev.jsonassert}:${dev.httpunit}:${dev.easyMock}:${dev.selenium.server}:${dev.objenesis}:${lib.js}:${lib.nekoHTML}:${lib.xerces}"/>
+
+  <target name="test" depends="compile,compile-test">
+    <echo message="Running test suite..." />
+    <junit printsummary="yes" haltonfailure="yes" fork="yes">
+      <classpath>
+        <pathelement path="${build}/class"/>
+        <pathelement path="${build}/test/class"/>
+        <pathelement path="${jars}:${testingJars}"/>
+      </classpath>
+      <sysproperty key="ca.nrc.cadc.util.PropertiesReader.dir" value="test"/>
+      <test name="ca.nrc.cadc.ac.server.ldap.LdapUserDAOTest" />
+      <test name="ca.nrc.cadc.ac.server.ldap.LdapGroupDAOTest" />
+      <formatter type="plain" usefile="false" />
+    </junit>
+  </target>
+
 </project>
diff --git a/projects/cadcAccessControl-Server/config/LdapConfig.dev.properties b/projects/cadcAccessControl-Server/config/LdapConfig.dev.properties
new file mode 100644
index 0000000000000000000000000000000000000000..0612a33b3987c7efbc6f19c01fb1b4d72efec14a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/config/LdapConfig.dev.properties
@@ -0,0 +1,13 @@
+# This are the configuration fields required by the Ldap ldap-dao unit tests
+# Tests are more accurate running on Port 636.  If it fails due to SSL/Security
+# issues, then make very sure the ca.crt (gimli2.cadc.dao.nrc.ca:~miscsw/ca.crt)
+# is installed in your Java Keystore:
+# scp gimli2.cadc.dao.nrc.ca:~miscsw/ca.crt /tmp/ca.crt
+# ${JAVA_HOME}/bin/keytool -importcert -keystore ${JAVA_HOME}/jre/lib/security/cacerts -file /tmp/ca.crt
+server = proc5-03.cadc.dao.nrc.ca
+port = 636
+proxyUser = testproxy
+usersDn = ou=Users,ou=ds,dc=testcanfar
+userRequestsDN = ou=UserRequests,ou=ds,dc=testcanfar
+groupsDn = ou=Groups,ou=ds,dc=testcanfar
+adminGroupsDn = ou=adminGroups,ou=ds,dc=testcanfar
\ No newline at end of file
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
index b2730be04082c9d4c9fb0bc56516e53ee677c748..380961d8e65420b6822e7f689e6d39829bd6bcdd 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
@@ -73,10 +73,7 @@ import java.security.Principal;
 import java.util.Collection;
 import java.util.Map;
 
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserAlreadyExistsException;
-import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.*;
 import ca.nrc.cadc.net.TransientException;
 
 import com.unboundid.ldap.sdk.DN;
@@ -91,7 +88,7 @@ public interface UserPersistence<T extends Principal>
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    Map<String, String> getUsers()
+    Map<String, PersonalDetails> getUsers()
             throws TransientException, AccessControlException;
     
     /**
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
index 1b85d39d0a180638f3d53bb03b21449960f35d7f..528a44dedad746a1e02cda2e564016361fcf4aa2 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
@@ -68,7 +68,6 @@
  */
 package ca.nrc.cadc.ac.server.ldap;
 
-import ca.nrc.cadc.ac.UserAlreadyExistsException;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.auth.NumericPrincipal;
 import ca.nrc.cadc.auth.OpenIdPrincipal;
@@ -79,18 +78,17 @@ import com.unboundid.ldap.sdk.LDAPException;
 import com.unboundid.ldap.sdk.ResultCode;
 import com.unboundid.ldap.sdk.SearchResult;
 import com.unboundid.ldap.sdk.SearchScope;
+import org.apache.log4j.Logger;
 
-import java.nio.file.FileAlreadyExistsException;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
 import java.security.AccessControlException;
 import java.security.AccessController;
 import java.security.GeneralSecurityException;
 import java.security.Principal;
 import java.util.Set;
-import javax.net.SocketFactory;
-import javax.net.ssl.SSLSocketFactory;
-import javax.security.auth.Subject;
-import javax.security.auth.x500.X500Principal;
-import org.apache.log4j.Logger;
 
 
 public abstract class LdapDAO
@@ -236,6 +234,7 @@ public abstract class LdapDAO
             throws TransientException
     {
     	logger.debug("Ldap result: " + code);
+        System.out.println("Ldap result: " + code);
 
         if (code == ResultCode.INSUFFICIENT_ACCESS_RIGHTS)
         {
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
index 23ad3b9a6cbc0728423df19a7028ffb58b24baa3..726b5f94cfda210c83e26d7f46327d29268f1baa 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
@@ -68,18 +68,6 @@
  */
 package ca.nrc.cadc.ac.server.ldap;
 
-import java.security.AccessControlException;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.security.auth.x500.X500Principal;
-
-import org.apache.log4j.Logger;
-
 import ca.nrc.cadc.ac.ActivatedGroup;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
@@ -89,7 +77,6 @@ import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.StringUtil;
-
 import com.unboundid.ldap.sdk.AddRequest;
 import com.unboundid.ldap.sdk.Attribute;
 import com.unboundid.ldap.sdk.DN;
@@ -106,6 +93,16 @@ import com.unboundid.ldap.sdk.SearchResult;
 import com.unboundid.ldap.sdk.SearchResultEntry;
 import com.unboundid.ldap.sdk.SearchScope;
 import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 public class LdapGroupDAO<T extends Principal> extends LdapDAO
 {
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
index fe3aba9a3c980198c512514e18d7582c3d3e16e1..b133948eb0ef553f34d805512682f339d7b4cce4 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
@@ -68,23 +68,41 @@
  */
 package ca.nrc.cadc.ac.server.ldap;
 
-import ca.nrc.cadc.ac.*;
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.PosixDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserAlreadyExistsException;
+import ca.nrc.cadc.ac.UserDetails;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.UserRequest;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.net.TransientException;
 import com.unboundid.ldap.sdk.AddRequest;
 import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.Control;
 import com.unboundid.ldap.sdk.DN;
 import com.unboundid.ldap.sdk.Filter;
 import com.unboundid.ldap.sdk.LDAPException;
 import com.unboundid.ldap.sdk.LDAPResult;
 import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.Modification;
+import com.unboundid.ldap.sdk.ModificationType;
+import com.unboundid.ldap.sdk.ModifyRequest;
 import com.unboundid.ldap.sdk.ResultCode;
 import com.unboundid.ldap.sdk.SearchRequest;
 import com.unboundid.ldap.sdk.SearchResult;
 import com.unboundid.ldap.sdk.SearchResultEntry;
 import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.SimpleBindRequest;
 import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
+import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest;
+import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedResult;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.x500.X500Principal;
 import java.security.AccessControlException;
 import java.security.Principal;
 import java.util.ArrayList;
@@ -93,8 +111,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import javax.security.auth.x500.X500Principal;
-import org.apache.log4j.Logger;
 
 
 public class LdapUserDAO<T extends Principal> extends LdapDAO
@@ -108,7 +124,6 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     protected static final String LDAP_OBJECT_CLASS = "objectClass";
     protected static final String LDAP_INET_ORG_PERSON = "inetOrgPerson";
     protected static final String LDAP_CADC_ACCOUNT = "cadcaccount";
-    protected static final String LDAP_POSIX_ACCOUNT = "posixaccount";
     protected static final String LDAP_NSACCOUNTLOCK = "nsaccountlock";
     protected static final String LDAP_MEMBEROF = "memberOf";
     protected static final String LDAP_ENTRYDN = "entrydn";
@@ -123,18 +138,12 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
     protected static final String LDAP_EMAIL = "email";
     protected static final String LDAP_INSTITUTE = "institute";
     protected static final String LDAP_UID = "uid";
-    protected static final String LDAP_UID_NUMBER = "uidNumber";
-    protected static final String LDAP_GID_NUMBER = "gidNumber";
-    protected static final String LDAP_HOME_DIRECTORY = "homeDirectory";
-    protected static final String LDAP_LOGIN_SHELL = "loginShell";
 
     private String[] userAttribs = new String[]
             {
                     LDAP_FIRST_NAME, LDAP_LAST_NAME, LDAP_ADDRESS, LDAP_CITY,
                     LDAP_COUNTRY,
-                    LDAP_EMAIL, LDAP_INSTITUTE, LDAP_UID, LDAP_UID_NUMBER,
-                    LDAP_GID_NUMBER,
-                    LDAP_HOME_DIRECTORY, LDAP_LOGIN_SHELL
+                    LDAP_EMAIL, LDAP_INSTITUTE, LDAP_UID
             };
     private String[] memberAttribs = new String[]
             {
@@ -163,6 +172,40 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         memberAttribs = tmp;
     }
 
+    /**
+     * Verifies the username and password for an existing User.
+     *
+     * @param username username to verify.
+     * @param password password to verify.
+     * @return User
+     * @throws TransientException
+     * @throws UserNotFoundException
+     */
+    public User<T> loginUser(final String username, final String password)
+        throws TransientException, UserNotFoundException
+    {
+        try
+        {
+            BindRequest bindRequest = new SimpleBindRequest(getUserDN(username), password);
+            BindResult bindResult = getConnection().bind(bindRequest);
+
+            if (bindResult != null && bindResult.getResultCode() == ResultCode.SUCCESS)
+            {
+                return getUser((T) new HttpPrincipal(username));
+            }
+            else
+            {
+                throw new AccessControlException("Invalid username or password");
+            }
+        }
+        catch (LDAPException e)
+        {
+            logger.debug("addUser Exception: " + e, e);
+
+            throw new RuntimeException("Unexpected LDAP exception", e);
+        }
+    }
+
     /**
      * @return
      * @throws TransientException
@@ -201,17 +244,15 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
 
             return userIDs;
         }
-        catch (LDAPException e1)
+        catch (LDAPException e)
         {
-            logger.debug("getCadcIDs Exception: " + e1, e1);
-            LdapDAO.checkLdapResult(e1.getResultCode());
+            logger.error("getCadcIDs Exception: " + e, e);
+            LdapDAO.checkLdapResult(e.getResultCode());
             throw new IllegalStateException("Unexpected exception: " +
-                                            e1.getMatchedDN(), e1);
+                                            e.getMatchedDN(), e);
         }
-
     }
 
-
     /**
      * Add the specified user..
      *
@@ -223,6 +264,70 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      */
     public User<T> addUser(final UserRequest<T> userRequest)
             throws TransientException, UserAlreadyExistsException
+    {
+        DN userDN;
+        try
+        {
+            userDN = getUserRequestsDN(userRequest.getUser().getUserID().getName());
+            addUser(userRequest, userDN);
+
+            // AD: Search results sometimes come incomplete if
+            // connection is not reset - not sure why.
+            getConnection().reconnect();
+            try
+            {
+                return getUser(userRequest.getUser().getUserID(), config.getUserRequestsDN());
+            }
+            catch (UserNotFoundException e)
+            {
+                throw new RuntimeException("BUG: new user " + userDN.toNormalizedString() +
+                    " not found because " + e.getMessage());
+            }
+        }
+        catch (LDAPException e)
+        {
+            logger.error("addUser Exception: " + e, e);
+            LdapUserDAO.checkUserLDAPResult(e.getResultCode());
+            throw new RuntimeException("Unexpected LDAP exception", e);
+        }
+    }
+
+    /**
+     * Package level method for unit testing to add a User directly to the
+     * Users tree.
+     */
+    User<T> newUser(final UserRequest<T> userRequest)
+        throws TransientException, UserAlreadyExistsException
+    {
+        DN userDN;
+        try
+        {
+            userDN = getUserDN(userRequest.getUser().getUserID().getName());
+            addUser(userRequest, userDN);
+
+            // AD: Search results sometimes come incomplete if
+            // connection is not reset - not sure why.
+            getConnection().reconnect();
+            try
+            {
+                return getUser(userRequest.getUser().getUserID(), config.getUsersDN());
+            }
+            catch (UserNotFoundException e)
+            {
+                throw new RuntimeException("BUG: new user " + userDN.toNormalizedString() +
+                                           " not found because " + e.getMessage());
+            }
+        }
+        catch (LDAPException e)
+        {
+            logger.error("newUser Exception: " + e, e);
+            LdapUserDAO.checkUserLDAPResult(e.getResultCode());
+            throw new RuntimeException("Unexpected LDAP exception", e);
+        }
+    }
+
+    private void addUser(final UserRequest<T> userRequest, final DN userDN)
+        throws TransientException, UserAlreadyExistsException
     {
         final User<T> user = userRequest.getUser();
         final Class userType = user.getUserID().getClass();
@@ -236,24 +341,22 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         try
         {
             // add new user
-            DN userDN = getUserRequestsDN(user.getUserID().getName());
             List<Attribute> attributes = new ArrayList<Attribute>();
             addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_INET_ORG_PERSON);
             addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_CADC_ACCOUNT);
             addAttribute(attributes, LDAP_COMMON_NAME, user.getUserID()
-                    .getName());
+                .getName());
             addAttribute(attributes, LDAP_DISTINGUISHED_NAME, userDN
-                    .toNormalizedString());
+                .toNormalizedString());
             addAttribute(attributes, LADP_USER_PASSWORD, userRequest
-                    .getPassword());
+                .getPassword());
 
             for (UserDetails details : user.details)
             {
                 if (details.getClass() == PersonalDetails.class)
                 {
                     PersonalDetails pd = (PersonalDetails) details;
-                    addAttribute(attributes, LDAP_FIRST_NAME, pd
-                            .getFirstName());
+                    addAttribute(attributes, LDAP_FIRST_NAME, pd.getFirstName());
                     addAttribute(attributes, LDAP_LAST_NAME, pd.getLastName());
                     addAttribute(attributes, LDAP_ADDRESS, pd.address);
                     addAttribute(attributes, LDAP_CITY, pd.city);
@@ -263,44 +366,18 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                 }
                 else if (details.getClass() == PosixDetails.class)
                 {
-                    PosixDetails pd = (PosixDetails) details;
-                    addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_POSIX_ACCOUNT);
-                    addAttribute(attributes, LDAP_UID, Long
-                            .toString(pd.getUid()));
-                    addAttribute(attributes, LDAP_UID_NUMBER, Long
-                            .toString(pd.getUid()));
-                    addAttribute(attributes, LDAP_GID_NUMBER, Long
-                            .toString(pd.getGid()));
-                    addAttribute(attributes, LDAP_HOME_DIRECTORY, pd
-                            .getHomeDirectory());
-                    addAttribute(attributes, LDAP_LOGIN_SHELL, pd.loginShell);
+                    throw new UnsupportedOperationException(
+                        "Support for users PosixDetails not available");
                 }
             }
 
             AddRequest addRequest = new AddRequest(userDN, attributes);
             LDAPResult result = getConnection().add(addRequest);
             LdapDAO.checkLdapResult(result.getResultCode());
-
-            // AD: Search results sometimes come incomplete if
-            // connection is not reset - not sure why.
-            getConnection().reconnect();
-            try
-            {
-                return getUser(user.getUserID(), config.getUserRequestsDN());
-            }
-            catch (UserNotFoundException e)
-            {
-                throw new RuntimeException("BUG: new user " + userDN
-                        .toNormalizedString() +
-                                           " not found, result " + result
-                                                   .getResultCode());
-            }
         }
         catch (LDAPException e)
         {
-            System.out.println("LDAPe: " + e);
-            System.out.println("LDAPrc: " + e.getResultCode());
-            logger.debug("addUser Exception: " + e, e);
+            logger.error("addUser Exception: " + e, e);
             LdapUserDAO.checkUserLDAPResult(e.getResultCode());
             throw new RuntimeException("Unexpected LDAP exception", e);
         }
@@ -376,9 +453,8 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
             throw new UserNotFoundException(msg);
         }
         User<T> user = new User<T>(userID);
-        user.getIdentities().add(new HttpPrincipal(searchResult
-                                                           .getAttributeValue(userLdapAttrib
-                                                                                      .get(HttpPrincipal.class))));
+        user.getIdentities().add(new HttpPrincipal(searchResult.getAttributeValue(
+            userLdapAttrib.get(HttpPrincipal.class))));
 
         String fname = searchResult.getAttributeValue(LDAP_FIRST_NAME);
         String lname = searchResult.getAttributeValue(LDAP_LAST_NAME);
@@ -391,18 +467,6 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                 .getAttributeValue(LDAP_INSTITUTE);
         user.details.add(personaDetails);
 
-        Long uid = searchResult.getAttributeValueAsLong(LDAP_UID_NUMBER);
-        Long gid = searchResult.getAttributeValueAsLong(LDAP_GID_NUMBER);
-        String homeDirectory = searchResult
-                .getAttributeValue(LDAP_HOME_DIRECTORY);
-        if (uid != null && gid != null && homeDirectory != null)
-        {
-            PosixDetails posixDetails = new PosixDetails(uid, gid, homeDirectory);
-            posixDetails.loginShell = searchResult
-                    .getAttributeValue(LDAP_LOGIN_SHELL);
-            user.details.add(posixDetails);
-        }
-
         return user;
     }
 
@@ -424,15 +488,16 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @return A map of string keys to string values.
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Map<String, String> getUsers()
+    public Map<String, PersonalDetails> getUsers()
             throws TransientException
     {
-        final Map<String, String> users = new HashMap<String, String>();
+        final Map<String, PersonalDetails> users =
+                new HashMap<String, PersonalDetails>();
 
         try
         {
-            final Filter filter = Filter.createPresenceFilter(LDAP_COMMON_NAME);
-            final String[] attributes = new String[]{LDAP_COMMON_NAME,
+            final Filter filter = Filter.createPresenceFilter(LDAP_UID);
+            final String[] attributes = new String[]{LDAP_UID,
                                                      LDAP_FIRST_NAME,
                                                      LDAP_LAST_NAME,
                                                      LDAP_NSACCOUNTLOCK};
@@ -450,10 +515,16 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
                 {
                     if (!next.hasAttribute(LDAP_NSACCOUNTLOCK))
                     {
-                        users.put(next.getAttributeValue(LDAP_COMMON_NAME),
-                                  next.getAttributeValue(LDAP_FIRST_NAME)
-                                  + " "
-                                  + next.getAttributeValue(LDAP_LAST_NAME));
+                        final String trimmedFirstName =
+                                next.getAttributeValue(LDAP_FIRST_NAME).trim();
+                        final String trimmedLastName =
+                                next.getAttributeValue(LDAP_LAST_NAME).trim();
+                        final String trimmedUID =
+                                next.getAttributeValue(LDAP_UID).trim();
+
+                        users.put(trimmedUID,
+                                  new PersonalDetails(trimmedFirstName,
+                                                      trimmedLastName));
                     }
                 }
             }
@@ -487,11 +558,95 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws TransientException     If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public User<T> modifyUser(User<T> user)
-            throws UserNotFoundException, TransientException,
-                   AccessControlException
+    public User<T> modifyUser(final User<T> user)
+            throws UserNotFoundException, TransientException, AccessControlException
     {
-        return null;
+        User existingUser = getUser(user.getUserID());
+
+        List<Modification> mods = new ArrayList<Modification>();
+        for (UserDetails details : user.details)
+        {
+            if (details.getClass() == PersonalDetails.class)
+            {
+                PersonalDetails pd = (PersonalDetails) details;
+                addModification(mods, LDAP_FIRST_NAME, pd.getFirstName());
+                addModification(mods, LDAP_LAST_NAME, pd.getLastName());
+                addModification(mods, LDAP_ADDRESS, pd.address);
+                addModification(mods, LDAP_CITY, pd.city);
+                addModification(mods, LDAP_COUNTRY, pd.country);
+                addModification(mods, LDAP_EMAIL, pd.email);
+                addModification(mods, LDAP_INSTITUTE, pd.institute);
+            }
+            else if (details.getClass() == PosixDetails.class)
+            {
+                throw new UnsupportedOperationException(
+                    "Support for users PosixDetails not available");
+            }
+        }
+
+        try
+        {
+            ModifyRequest modifyRequest = new ModifyRequest(getUserDN(user), mods);
+            modifyRequest.addControl(
+                new ProxiedAuthorizationV2RequestControl(
+                    "dn:" + getSubjectDN().toNormalizedString()));
+            LdapDAO.checkLdapResult(getConnection().modify(modifyRequest).getResultCode());
+        }
+        catch (LDAPException e)
+        {
+            e.printStackTrace();
+            logger.debug("Modify Exception: " + e, e);
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+        try
+        {
+            return getUser(user.getUserID());
+        }
+        catch (UserNotFoundException e)
+        {
+            throw new RuntimeException(
+                "BUG: modified user not found (" + user.getUserID() + ")");
+        }
+    }
+
+    /**
+     * Update a user's password. The given user and authenticating user must match.
+     *
+     * @param user
+     * @param oldPassword   current password.
+     * @param newPassword   new password.
+     * @throws UserNotFoundException If the given user does not exist.
+     * @throws TransientException   If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public void setPassword(User<T> user, final String oldPassword, final String newPassword)
+        throws UserNotFoundException, TransientException, AccessControlException
+    {
+        try
+        {
+            DN userDN = getUserDN(user);
+            DN subjectDN =  getSubjectDN();
+            if (!userDN.equals(subjectDN))
+            {
+                throw new AccessControlException("Given user and authenticating user do not match");
+            }
+
+            ProxiedAuthorizationV2RequestControl control =
+                new ProxiedAuthorizationV2RequestControl("dn:" + getSubjectDN().toNormalizedString());
+            Control[] controls = new Control[] {control};
+
+            PasswordModifyExtendedRequest passwordModifyRequest =
+                new PasswordModifyExtendedRequest(userDN.toNormalizedString(), oldPassword, newPassword, controls);
+
+            PasswordModifyExtendedResult passwordModifyResult = (PasswordModifyExtendedResult)
+                getConnection().processExtendedOperation(passwordModifyRequest);
+            LdapDAO.checkLdapResult(passwordModifyResult.getResultCode());
+        }
+        catch (LDAPException e)
+        {
+            logger.debug("setPassword Exception: " + e);
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
     }
 
     /**
@@ -595,7 +750,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
      * @throws TransientException     If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
      */
-    public boolean isMember(T userID, String groupID)
+    public boolean isMember(final T userID, final String groupID)
             throws UserNotFoundException, TransientException,
                    AccessControlException
     {
@@ -767,7 +922,7 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         throw new IllegalArgumentException(userID + " not a valid user ID");
     }
 
-    void addAttribute(List<Attribute> attributes, final String name, final String value)
+    private void addAttribute(List<Attribute> attributes, final String name, final String value)
     {
         if (value != null && !value.isEmpty())
         {
@@ -775,6 +930,18 @@ public class LdapUserDAO<T extends Principal> extends LdapDAO
         }
     }
 
+    private void addModification(List<Modification> mods, final String name, final String value)
+    {
+        if (value != null && !value.isEmpty())
+        {
+            mods.add(new Modification(ModificationType.REPLACE, name, value));
+        }
+        else
+        {
+            mods.add(new Modification(ModificationType.REPLACE, name));
+        }
+    }
+
     /**
      * Checks the Ldap result code, and if the result is not SUCCESS,
      * throws an appropriate exception. This is the place to decide on
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
index 452598a10ea4271fba4fdd547cce682606ae80de..af7662460e342b8ae107af471b1b57bf0aa8d019 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
@@ -68,10 +68,7 @@
  */
 package ca.nrc.cadc.ac.server.ldap;
 
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserAlreadyExistsException;
-import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.*;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.net.TransientException;
 import com.unboundid.ldap.sdk.DN;
@@ -100,7 +97,7 @@ public class LdapUserPersistence<T extends Principal>
         }
     }
     
-    public Map<String, String> getUsers()
+    public Map<String, PersonalDetails> getUsers()
         throws TransientException, AccessControlException
     {
         LdapUserDAO<T> userDAO = null;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
index df069f8bc4d5ca996d888616604e2198d4d4abed..a6409ad49a03dfa8167385d66256577d3ae235f3 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
@@ -66,31 +66,15 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
-
-import java.io.IOException;
-import java.security.AccessControlContext;
-import java.security.AccessControlException;
-import java.security.AccessController;
-import java.security.Principal;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Iterator;
-import java.util.Set;
-
-import javax.security.auth.Subject;
-import javax.security.auth.x500.X500Principal;
-import javax.servlet.http.HttpServletResponse;
-
-import org.apache.log4j.Logger;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupNotFoundException;
-import ca.nrc.cadc.ac.xml.GroupsWriter;
 import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.ac.server.PluginFactory;
 import ca.nrc.cadc.ac.server.RequestValidator;
+import ca.nrc.cadc.ac.xml.GroupsWriter;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.net.TransientException;
@@ -100,9 +84,23 @@ import ca.nrc.cadc.uws.server.JobRunner;
 import ca.nrc.cadc.uws.server.JobUpdater;
 import ca.nrc.cadc.uws.server.SyncOutput;
 import ca.nrc.cadc.uws.util.JobLogInfo;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.security.AccessControlContext;
+import java.security.AccessControlException;
+import java.security.AccessController;
+import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Set;
 
 public class ACSearchRunner implements JobRunner
 {
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
index c842e9708c09ea603ebc77c21173802586bd1bea..61669fa27d74b9f372b10fe30c4225c1e63d6a6b 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberAction.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
similarity index 98%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
index 7c6873401e1aa28c94b36e0980ec70bedf72bea5..cdf6093abea691a7412e1fd0a4a6bacd36c12cd4 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberAction.java
@@ -66,18 +66,16 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.MemberAlreadyExistsException;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
-import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 public class AddUserMemberAction extends GroupsAction
 {
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
index 47cdc9bc198a5bdc0e01c0e93aa501cb9bffc0bb..f282d722627e25796756e8921ae77ca2fb4f3940 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/CreateGroupAction.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.InputStream;
 import java.util.ArrayList;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
index 8f619fa381cc12469cf9ff0b196c280364d22571..2c96f279f30a5108de5be505761094a2c4e5bf9a 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupAction.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.util.ArrayList;
 
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
index 97a2b52ea289c730ff74ae3637cfb16a0e6b205d..3d7d9646f6692d46f3c76e2943702a7d55707bb2 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupAction.java
@@ -65,7 +65,7 @@
  *  $Revision: 4 $
  *
  ************************************************************************
- */package ca.nrc.cadc.ac.server.web;
+ */package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.server.GroupPersistence;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
index a29e5fcf29a37ebf66ded5fd96cff4edbe77f26c..a62ef9526d1ff961f6f9b0a88ea82283762f061c 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesAction.java
@@ -67,7 +67,7 @@
  ************************************************************************
  */
 
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.Writer;
 import java.util.Collection;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
index 512610114aa61aa3cc24d30a2fd62c3922f93e7c..38739a0849e2755fdcd6c42503a9e402a3cae8dd 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupLogInfo.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.log.ServletLogInfo;
 import java.util.List;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java
index 42c0ac3cf8138d192991ae40b80da515016fd1dc..4ef1811573d47b135823e0e584ae9b2ace14b720 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsAction.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.IOException;
 import java.security.AccessControlException;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
index 9263dde60b2cb5f343596eae7a6c41064ffe08a4..c2aaf942be82e190f33b7f9e1f7cb40aa6fafe97 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionFactory.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.IOException;
 import java.net.URL;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java
index dd62ed5cdb871629bb86aa975b1a24e7b583e492..cd4682325e14892bdb73c8cd96c7ed1782368730 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupsServlet.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.IOException;
 
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
similarity index 99%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
index 425951d7c80820977ec32eaef9f7af85d2a967d9..444e335dc3594ec7543872a5f77db25ef0cc4c32 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ModifyGroupAction.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import java.io.InputStream;
 import java.util.ArrayList;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
similarity index 98%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
index 269984b17e2d626ed58551a7c5ad130349405cc4..a05d17f93d73a061e96bb44975b3f68548b3b7c5 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberAction.java
@@ -66,14 +66,13 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Set;
 
 public class RemoveGroupMemberAction extends GroupsAction
 {
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
similarity index 98%
rename from projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java
rename to projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
index 9b4d34f73c8af063650ac3ecf5245ba2c7058b5b..20dcf8b4a32af6119d4033ea5b72db0389218ba1 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberAction.java
@@ -66,13 +66,12 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.MemberNotFoundException;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
-import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import java.security.Principal;
 import java.util.ArrayList;
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java
index 60f75a3ec9c9d070814702c3686250d8c2998c27..df6880b92f0ab5c5bf2116e956b3d88bfc7c5798 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersAction.java
@@ -68,6 +68,17 @@
  */
 package ca.nrc.cadc.ac.server.web.users;
 
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.UserRequest;
+import ca.nrc.cadc.ac.server.PluginFactory;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.net.TransientException;
+import org.apache.log4j.Logger;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Writer;
@@ -77,18 +88,6 @@ import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.util.Map;
 
-import javax.security.auth.Subject;
-import javax.servlet.http.HttpServletResponse;
-
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserRequest;
-import org.apache.log4j.Logger;
-
-import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.server.PluginFactory;
-import ca.nrc.cadc.ac.server.UserPersistence;
-import ca.nrc.cadc.net.TransientException;
-
 public abstract class UsersAction
     implements PrivilegedExceptionAction<Object>
 {
@@ -278,7 +277,7 @@ public abstract class UsersAction
      *
      * @param users         The Map of user IDs to names.
      */
-    protected final void writeUsers(final Map<String, String> users)
+    protected final void writeUsers(final Map<String, PersonalDetails> users)
             throws IOException
     {
         response.setContentType(acceptedContentType);
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java
index 926ce997fb15e7cd01df865088749b3f82c429e1..c3f2e53c75821f6de8d170d5d1a02393196ca316 100644
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/users/UsersServlet.java
@@ -173,13 +173,14 @@ public class UsersServlet extends HttpServlet
     {
         final String requestedContentType = request.getHeader("Accept");
 
-        if (!UsersAction.JSON_CONTENT_TYPE.equals(requestedContentType))
+        if (StringUtil.hasText(requestedContentType)
+            && requestedContentType.contains(UsersAction.JSON_CONTENT_TYPE))
         {
-            return UsersAction.DEFAULT_CONTENT_TYPE;
+            return UsersAction.JSON_CONTENT_TYPE;
         }
         else
         {
-            return requestedContentType;
+            return UsersAction.DEFAULT_CONTENT_TYPE;
         }
     }
 }
diff --git a/projects/cadcAccessControl-Server/test/LdapConfig.test.properties b/projects/cadcAccessControl-Server/test/LdapConfig.test.properties
index 2354a157763992f801c862d5d1945663edd9b055..99ee0cc862a105684c6bd2cad55cba5c8ed42640 100644
--- a/projects/cadcAccessControl-Server/test/LdapConfig.test.properties
+++ b/projects/cadcAccessControl-Server/test/LdapConfig.test.properties
@@ -1,9 +1,13 @@
 # This are the configuration fields required by the Ldap ldap-dao unit tests
+# Tests are more accurate running on Port 636.  If it fails due to SSL/Security
+# issues, then make very sure the ca.crt (gimli2.cadc.dao.nrc.ca:~miscsw/ca.crt)
+# is installed in your Java Keystore:
+# scp gimli2.cadc.dao.nrc.ca:~miscsw/ca.crt /tmp/ca.crt
+# ${JAVA_HOME}/bin/keytool -importcert -keystore ${JAVA_HOME}/jre/lib/security/cacerts -file /tmp/ca.crt
 server = proc5-03.cadc.dao.nrc.ca
-port = 636
-proxyUser = webproxy
-usersDn = ou=Users,ou=ds,dc=canfar,dc=net
+port = 389
+proxyUser = testproxy
+usersDn = ou=Users,ou=ds,dc=testcanfar
 userRequestsDN = ou=UserRequests,ou=ds,dc=testcanfar
-newUsersDn = ou=NewUsers,ou=ds,dc=canfar,dc=net
-groupsDn = ou=Groups,ou=ds,dc=canfar,dc=net
-adminGroupsDn = ou=adminGroups,ou=ds,dc=canfar,dc=net
\ No newline at end of file
+groupsDn = ou=Groups,ou=ds,dc=testcanfar
+adminGroupsDn = ou=adminGroups,ou=ds,dc=testcanfar
\ No newline at end of file
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java
index 94760917ca297b709b57a06d0700769e95dd9d05..e54ea3da63d1689021d071ae82a86d58c12952f6 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java
@@ -69,11 +69,11 @@
 package ca.nrc.cadc.ac.server;
 
 import ca.nrc.cadc.ac.Role;
-import ca.nrc.cadc.ac.server.web.AddUserMemberActionTest;
+import ca.nrc.cadc.ac.server.web.groups.AddUserMemberActionTest;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.util.Log4jInit;
 import ca.nrc.cadc.uws.Parameter;
-import java.security.Principal;
+
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.log4j.Level;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
index bad3d32ac6ec804357f8bae67f88707fdca8d619..2f9bcdf27a8fb962fd5d1645f997975200956706 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
@@ -67,23 +67,6 @@
 
 package ca.nrc.cadc.ac.server.ldap;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.security.AccessControlException;
-import java.security.PrivilegedExceptionAction;
-import java.util.Collection;
-
-import javax.security.auth.Subject;
-import javax.security.auth.x500.X500Principal;
-
-import org.apache.log4j.Level;
-import org.apache.log4j.Logger;
-import org.junit.BeforeClass;
-import org.junit.Test;
-
 import ca.nrc.cadc.ac.ActivatedGroup;
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
@@ -91,8 +74,23 @@ import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.GroupProperty;
 import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.util.Log4jInit;
 import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.util.Log4jInit;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import java.security.AccessControlException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class LdapGroupDAOTest extends AbstractLdapDAOTest
 {
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
index aa47a3091287bdbdb006f9876b389df026ce05d6..0e592c8012b889cd4b425b5bae0b3089a647f1f4 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
@@ -86,9 +86,13 @@ import java.security.Principal;
 import java.security.PrivilegedExceptionAction;
 import java.util.Collection;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-public class LdapUserDAOTest extends AbstractLdapDAOTest
+public class LdapUserDAOTest<T extends Principal> extends AbstractLdapDAOTest
 {
     private static final Logger log = Logger.getLogger(LdapUserDAOTest.class);
 
@@ -97,10 +101,10 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
     static String testUserDN;
     static User<X500Principal> testUser;
     static LdapConfig config;
-    
+
     @BeforeClass
     public static void setUpBeforeClass()
-        throws Exception
+            throws Exception
     {
         Log4jInit.setLevel("ca.nrc.cadc.ac", Level.DEBUG);
 
@@ -118,12 +122,12 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
     {
         return new LdapUserDAO(config);
     }
-    
+
     String getUserID()
     {
         return "CadcDaoTestUser-" + System.currentTimeMillis();
     }
-    
+
     /**
      * Test of addUser method, of class LdapUserDAO.
      */
@@ -143,7 +147,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
         User<HttpPrincipal> actual = getUserDAO().addUser(userRequest);
         check(expected, actual);
     }
-    
+
     /**
      * Test of getUser method, of class LdapUserDAO.
      */
@@ -156,13 +160,14 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
         // do everything as owner
         Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
         {
-            public Object run() throws Exception
+            public Object run()
+                throws Exception
             {
                 try
                 {
                     User<X500Principal> actual = getUserDAO().getUser(testUser.getUserID());
                     check(testUser, actual);
-                    
+
                     return null;
                 }
                 catch (Exception e)
@@ -189,19 +194,24 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
             public Object run() throws Exception
             {
                 try
-                {            
-                    Collection<DN> groups = getUserDAO().getUserGroups(testUser.getUserID(), false);
-                    assertNotNull(groups);
-                    assertTrue(!groups.isEmpty());
+                {
+                    Collection<DN> groups = getUserDAO().getUserGroups(testUser.getUserID(),
+                                                       false);
+                    assertNotNull("Groups should not be null.", groups);
+
                     for (DN groupDN : groups)
+                    {
                         log.debug(groupDN);
-                    
-                    groups = getUserDAO().getUserGroups(testUser.getUserID(), true);
-                    assertNotNull(groups);
-                    assertTrue(!groups.isEmpty());
+                    }
+
+                    groups = getUserDAO().getUserGroups(testUser.getUserID(),
+                                                        true);
+                    assertNotNull("Groups should not be null.", groups);
                     for (DN groupDN : groups)
+                    {
                         log.debug(groupDN);
-                    
+                    }
+
                     return null;
                 }
                 catch (Exception e)
@@ -211,7 +221,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
             }
         });
     }
-    
+
     /**
      * Test of getUserGroups method, of class LdapUserDAO.
      */
@@ -227,14 +237,15 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
             public Object run() throws Exception
             {
                 try
-                {   
+                {
                     boolean isMember = getUserDAO().isMember(testUser.getUserID(), "foo");
-                    assertFalse(isMember);
-                    
-                    String groupDN = "cn=cadcdaotestgroup1," + config.getGroupsDN();
-                    isMember = getUserDAO().isMember(testUser.getUserID(), groupDN);
-                    assertTrue(isMember);
-                    
+                    assertFalse("Membership should not exist.", isMember);
+
+                    String  groupDN = "cn=cadcdaotestgroup1," + config.getGroupsDN();
+                    isMember = getUserDAO().isMember(testUser.getUserID(),
+                                                     groupDN);
+                    assertTrue("Membership should exist.", isMember);
+
                     return null;
                 }
                 catch (Exception e)
@@ -244,7 +255,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
             }
         });
     }
-    
+
     /**
      * Test of getMember.
      */
@@ -260,7 +271,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
             public Object run() throws Exception
             {
                 try
-                {   
+                {
                     User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
                     check(testUser, actual);
                     return null;
@@ -271,7 +282,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
                 }
             }
         });
-        
+
         // should also work as a different user
         subject = new Subject();
         subject.getPrincipals().add(new HttpPrincipal("CadcDaoTest2"));
@@ -279,10 +290,11 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
         // do everything as owner
         Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
         {
-            public Object run() throws Exception
+            public Object run()
+                throws Exception
             {
                 try
-                {   
+                {
                     User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
                     check(testUser, actual);
                     return null;
@@ -290,11 +302,11 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
                 catch (Exception e)
                 {
                     throw new Exception("Problems", e);
-                } 
+                }
             }
         });
     }
-    
+
     /**
      * Test of testGetCadcUserIDs.
      */
@@ -302,36 +314,37 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
     public void testGetCadcUserIDs() throws Exception
     {
         Subject subject = new Subject();
-       
-        
+
         // anonymous access
-        int users1 = (Integer)Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
-        {
-            public Object run() throws Exception
-            {
-                try
-                {            
-                    
-                    int count = getUserDAO().getCadcIDs().size();
-                    assertTrue(count > 0);
-                    return count;
-                }
-                catch (Exception e)
+        int users1 = (Integer) Subject
+                .doAs(subject, new PrivilegedExceptionAction<Object>()
                 {
-                    throw new Exception("Problems", e);
-                }
-            }
-        });
-        
+                    public Object run() throws Exception
+                    {
+                        try
+                        {
+
+                            int count = getUserDAO().getCadcIDs().size();
+                            assertTrue(count > 0);
+                            return count;
+                        }
+                        catch (Exception e)
+                        {
+                            throw new Exception("Problems", e);
+                        }
+                    }
+                });
+
         // authenticated access
         subject.getPrincipals().add(testUser.getUserID());
-        int users2 = (Integer)Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        int users2 = (Integer) Subject
+                .doAs(subject, new PrivilegedExceptionAction<Object>()
                 {
                     public Object run() throws Exception
                     {
                         try
-                        {            
-                            
+                        {
+
                             int count = getUserDAO().getCadcIDs().size();
                             assertTrue(count > 0);
                             return count;
@@ -343,27 +356,238 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
                     }
                 });
         assertEquals("User listing should be independent of the access type",
-                users1, users2);
+            users1, users2);
     }
-    
+
+//    @Test
+    public void testSetPassword() throws Exception
+    {
+//        LDAPConnection connection =
+//            new LDAPConnection(SocketFactory.getDefault(), config.getServer(), config.getPort());
+//        connection.bind(config.getAdminUserDN(), config.getAdminPasswd());
+//
+//        // Create an SSLUtil instance that is configured to trust any certificate.
+//        SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+//        SSLContext sslContext = sslUtil.createSSLContext();
+//        StartTLSExtendedRequest startTLSRequest = new StartTLSExtendedRequest(sslContext);
+//        ExtendedResult startTLSResult = connection.processExtendedOperation(startTLSRequest);
+//        LDAPTestUtils.assertResultCodeEquals(startTLSResult, ResultCode.SUCCESS);
+
+        // Create a test user with a known password
+        final User<HttpPrincipal> testUser2;
+        final String username = getUserID();
+        final String password = "foo";
+        final String newPassword = "bar";
+
+        HttpPrincipal principal = new HttpPrincipal(username);
+        testUser2 = new User<HttpPrincipal>(principal);
+        testUser2.getIdentities().add(principal);
+        testUser2.details.add(new PersonalDetails("firstName", "lastName"));
+        final UserRequest userRequest = new UserRequest(testUser2, password);
+
+        // add the user
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser2.getUserID());
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    return getUserDAO().newUser(userRequest);
+                }
+                catch (Exception e)
+                {
+                    fail("exception updating user: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+
+        // authenticate new username and password
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    getUserDAO().loginUser(username, password);
+                }
+                catch (Exception e)
+                {
+                    fail("exception during login: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+
+        // anonymous access should throw exception
+        subject = new Subject();
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    getUserDAO().setPassword(testUser2, password, newPassword);
+                    fail("should throw exception if subject and user are not the same");
+                }
+                catch (Exception ignore){}
+                return null;
+            }
+        });
+
+        // change the password
+        subject.getPrincipals().add(testUser2.getUserID());
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    getUserDAO().setPassword(testUser2, password, newPassword);
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                    fail("exception setting password: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+
+        // verify new password
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    getUserDAO().loginUser(username, password);
+                }
+                catch (Exception e)
+                {
+                    fail("exception during login: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+
+    }
+
+    @Test
+    public void testUpdateUser() throws Exception
+    {
+        // Create a test user
+        final User<HttpPrincipal> testUser2;
+        final String username = getUserID();
+        final String password = "foo";
+
+        HttpPrincipal principal = new HttpPrincipal(username);
+        testUser2 = new User<HttpPrincipal>(principal);
+        testUser2.getIdentities().add(principal);
+        testUser2.details.add(new PersonalDetails("firstName", "lastName"));
+        final UserRequest userRequest = new UserRequest(testUser2, password);
+
+        // add the user
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser2.getUserID());
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    return getUserDAO().newUser(userRequest);
+                }
+                catch (Exception e)
+                {
+                    fail("exception updating user: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+
+        // update the user
+        for (UserDetails details : testUser2.details)
+        {
+            if (details instanceof PersonalDetails)
+            {
+                PersonalDetails pd = (PersonalDetails) details;
+                pd.email = "email2";
+                pd.address = "address2";
+                pd.institute = "institute2";
+                pd.city = "city2";
+                pd.country = "country2";
+            }
+        }
+
+        // anonymous access should throw exception
+        subject = new Subject();
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    getUserDAO().modifyUser(testUser2);
+                    fail("should throw exception if subject and user are not the same");
+                }
+                catch (Exception ignore)
+                {
+                }
+                return null;
+            }
+        });
+
+        // update the user
+        subject.getPrincipals().add(testUser2.getUserID());
+        User<? extends Principal> updatedUser =
+            (User<? extends Principal>) Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run()
+                throws Exception
+            {
+                try
+                {
+                    return getUserDAO().modifyUser(testUser2);
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                    fail("exception updating user: " + e.getMessage());
+                }
+                return null;
+            }
+        });
+        assertNotNull(updatedUser);
+        check(testUser2, updatedUser);
+    }
+
     private static void check(final User<? extends Principal> user1, final User<? extends Principal> user2)
     {
         assertEquals(user1, user2);
         assertEquals(user1.details, user2.details);
         assertEquals(user1.details.size(), user2.details.size());
         assertEquals(user1.getIdentities(), user2.getIdentities());
-        for(UserDetails d1 : user1.details)
+        for (UserDetails d1 : user1.details)
         {
             assertTrue(user2.details.contains(d1));
-            if(d1 instanceof PersonalDetails)
+            if (d1 instanceof PersonalDetails)
             {
-                PersonalDetails pd1 = (PersonalDetails)d1;
+                PersonalDetails pd1 = (PersonalDetails) d1;
                 boolean found = false;
-                for(UserDetails d2 : user2.details)
+                for (UserDetails d2 : user2.details)
                 {
-                    if(d2 instanceof PersonalDetails)
+                    if (d2 instanceof PersonalDetails)
                     {
-                        PersonalDetails pd2 = (PersonalDetails)d2;
+                        PersonalDetails pd2 = (PersonalDetails) d2;
                         assertEquals(pd1, pd2); // already done in contains above but just in case
                         assertEquals(pd1.address, pd2.address);
                         assertEquals(pd1.city, pd2.city);
@@ -376,7 +600,7 @@ public class LdapUserDAOTest extends AbstractLdapDAOTest
                 }
             }
         }
-        
+
     }
-    
+
 }
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
similarity index 99%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
index eb092c7f48d4e86880a3f9b2eafb2e840ecec592..8550e9ba2dfa13797a5da1a34c5a93fcd8261c10 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddGroupMemberActionTest.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
similarity index 98%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
index 6488fd768583dd7d06841a16547158a6c8b32b6a..1cdb6f4fc1cf397c8388bbb7322fd39cbb845e61 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/AddUserMemberActionTest.java
@@ -66,10 +66,9 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.MemberAlreadyExistsException;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
@@ -77,7 +76,7 @@ import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.util.Log4jInit;
 import java.security.Principal;
-import javax.security.auth.x500.X500Principal;
+
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
similarity index 95%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
index 4562671a6951e209e759a96993499782ede5a9e8..0994610208aab34a25f8b521508c6146283df352 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/DeleteGroupActionTest.java
@@ -66,22 +66,15 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.MemberAlreadyExistsException;
-import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
-import ca.nrc.cadc.ac.server.UserPersistence;
-import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.util.Log4jInit;
 import java.security.Principal;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
similarity index 99%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
index 0cb59383950ce877c3c5c88a4ee1da506adecce7..1f2d4d67b0f71e4a2734230b8308f04cd27d5353 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GetGroupNamesActionTest.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.util.Log4jInit;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
similarity index 99%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
index f62d030cc6b0f9131a9681250664fbee8a97444c..3a9b19ccb053c960256ea8c67cd32fe39dac6544 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupActionFactoryTest.java
@@ -65,7 +65,7 @@
  ************************************************************************
  */
 
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -77,8 +77,6 @@ import org.junit.Test;
 
 import ca.nrc.cadc.util.Log4jInit;
 
-import java.net.URL;
-
 public class GroupActionFactoryTest
 {
     private final static Logger log = Logger.getLogger(GroupActionFactoryTest.class);
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java
similarity index 98%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java
index 08b28e9725675f4d955688e194198862144b9f42..def8852585eaf9d5102a31ede3712bb7214504f0 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/GroupsActionTest.java
@@ -66,7 +66,7 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
@@ -76,18 +76,14 @@ import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.Log4jInit;
 
-import java.io.ByteArrayOutputStream;
-import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.security.AccessControlException;
-import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 import static org.junit.Assert.*;
 
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
similarity index 97%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
index 7ccc5abb9753fd6ad5f1336e1ddfd4b46d8c1638..749d0076eed5527337e293ac48f4b11951d54a55 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveGroupMemberActionTest.java
@@ -66,10 +66,9 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.util.Log4jInit;
@@ -77,9 +76,6 @@ import java.security.Principal;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
similarity index 98%
rename from projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java
rename to projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
index fea4de56d5d4ff31e3f08e473eeb5d8f8111fc12..05609091e46dba3e1b60d8c307622578d75b62b3 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/groups/RemoveUserMemberActionTest.java
@@ -66,10 +66,9 @@
  *
  ************************************************************************
  */
-package ca.nrc.cadc.ac.server.web;
+package ca.nrc.cadc.ac.server.web.groups;
 
 import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.MemberAlreadyExistsException;
 import ca.nrc.cadc.ac.MemberNotFoundException;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.server.GroupPersistence;
@@ -80,9 +79,6 @@ import java.security.Principal;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
 import org.easymock.EasyMock;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import static org.junit.Assert.*;
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java
index e8f7004fda1c3462fdf4b82e616b2227df24bc00..f1dc7a29aeb521d739a16325c62b97af3a83510a 100644
--- a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/users/GetUsersActionTest.java
@@ -69,6 +69,7 @@
 package ca.nrc.cadc.ac.server.web.users;
 
 
+import ca.nrc.cadc.ac.PersonalDetails;
 import ca.nrc.cadc.ac.server.UserPersistence;
 import ca.nrc.cadc.auth.HttpPrincipal;
 import org.apache.log4j.Level;
@@ -112,11 +113,13 @@ public class GetUsersActionTest
                 createMock(HttpServletResponse.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, String> userEntries = new HashMap<String, String>();
+        final Map<String, PersonalDetails> userEntries =
+                new HashMap<String, PersonalDetails>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i, "USER " + i);
+            userEntries.put("USER_" + i,
+                            new PersonalDetails("USER", Integer.toString(i)));
         }
 
         final GetUsersAction testSubject = new GetUsersAction(null)
@@ -143,7 +146,7 @@ public class GetUsersActionTest
         testSubject.doAction(null, mockResponse);
 
         final JSONArray expected =
-                new JSONArray("[{\"id\":\"USER_1\",\"name\":\"USER 1\"},{\"id\":\"USER_3\",\"name\":\"USER 3\"},{\"id\":\"USER_2\",\"name\":\"USER 2\"},{\"id\":\"USER_4\",\"name\":\"USER 4\"},{\"id\":\"USER_5\",\"name\":\"USER 5\"}]");
+                new JSONArray("[{\"id\":\"USER_1\",\"firstName\":\"USER\",\"lastName\":\"1\"},{\"id\":\"USER_3\",\"firstName\":\"USER\",\"lastName\":\"3\"},{\"id\":\"USER_2\",\"firstName\":\"USER\",\"lastName\":\"2\"},{\"id\":\"USER_4\",\"firstName\":\"USER\",\"lastName\":\"4\"},{\"id\":\"USER_5\",\"firstName\":\"USER\",\"lastName\":\"5\"}]");
         final JSONArray result = new JSONArray(writer.toString());
 
         JSONAssert.assertEquals(expected, result, true);
@@ -158,11 +161,13 @@ public class GetUsersActionTest
                 createMock(HttpServletResponse.class);
         final UserPersistence<HttpPrincipal> mockUserPersistence =
                 createMock(UserPersistence.class);
-        final Map<String, String> userEntries = new HashMap<String, String>();
+        final Map<String, PersonalDetails> userEntries =
+                new HashMap<String, PersonalDetails>();
 
         for (int i = 1; i <= 5; i++)
         {
-            userEntries.put("USER_" + i, "USER " + i);
+            userEntries.put("USER_" + i,
+                            new PersonalDetails("USER", Integer.toString(i)));
         }
 
         final GetUsersAction testSubject = new GetUsersAction(null)
@@ -189,19 +194,24 @@ public class GetUsersActionTest
         final String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" +
                                 "<users>\r\n" +
                                 "  <user id=\"USER_1\">\r\n" +
-                                "    <name>USER 1</name>\r\n" +
+                                "    <firstName>USER</firstName>\r\n" +
+                                "    <lastName>1</lastName>\r\n" +
                                 "  </user>\r\n" +
                                 "  <user id=\"USER_3\">\r\n" +
-                                "    <name>USER 3</name>\r\n" +
+                                "    <firstName>USER</firstName>\r\n" +
+                                "    <lastName>3</lastName>\r\n" +
                                 "  </user>\r\n" +
                                 "  <user id=\"USER_2\">\r\n" +
-                                "    <name>USER 2</name>\r\n" +
+                                "    <firstName>USER</firstName>\r\n" +
+                                "    <lastName>2</lastName>\r\n" +
                                 "  </user>\r\n" +
                                 "  <user id=\"USER_4\">\r\n" +
-                                "    <name>USER 4</name>\r\n" +
+                                "    <firstName>USER</firstName>\r\n" +
+                                "    <lastName>4</lastName>\r\n" +
                                 "  </user>\r\n" +
                                 "  <user id=\"USER_5\">\r\n" +
-                                "    <name>USER 5</name>\r\n" +
+                                "    <firstName>USER</firstName>\r\n" +
+                                "    <lastName>5</lastName>\r\n" +
                                 "  </user>\r\n" +
                                 "</users>\r\n";
         final String result = writer.toString();
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
index 159e48eaddffdb4f62b6fea3a1eea8af3b29fc33..8f4eab02036b1155294d80566c74a434875ad142 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -68,12 +68,7 @@
  */
 package ca.nrc.cadc.ac.client;
 
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
+import java.io.*;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -93,16 +88,14 @@ import javax.net.ssl.HttpsURLConnection;
 import javax.net.ssl.SSLSocketFactory;
 import javax.security.auth.Subject;
 
+import ca.nrc.cadc.ac.*;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.util.StringUtil;
 import org.apache.log4j.Logger;
 
-import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupAlreadyExistsException;
-import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.xml.GroupReader;
 import ca.nrc.cadc.ac.xml.GroupWriter;
 import ca.nrc.cadc.ac.xml.GroupsReader;
-import ca.nrc.cadc.ac.Role;
-import ca.nrc.cadc.ac.UserNotFoundException;
 import ca.nrc.cadc.auth.AuthenticationUtil;
 import ca.nrc.cadc.auth.SSLUtil;
 import ca.nrc.cadc.net.HttpDownload;
@@ -110,6 +103,7 @@ import ca.nrc.cadc.net.HttpPost;
 import ca.nrc.cadc.net.HttpUpload;
 import ca.nrc.cadc.net.InputStreamWrapper;
 import ca.nrc.cadc.net.NetUtil;
+import org.json.JSONObject;
 
 
 /**
@@ -119,21 +113,21 @@ import ca.nrc.cadc.net.NetUtil;
 public class GMSClient
 {
     private static final Logger log = Logger.getLogger(GMSClient.class);
-    
+
     // socket factory to use when connecting
     private SSLSocketFactory sslSocketFactory;
     private SSLSocketFactory mySocketFactory;
-    
+
     private String baseURL;
 
     /**
      * Constructor.
-     * 
+     *
      * @param baseURL The URL of the supporting access control web service
-     * obtained from the registry.
+     *                obtained from the registry.
      */
-    public GMSClient(String baseURL)
-        throws IllegalArgumentException
+    public GMSClient(final String baseURL)
+            throws IllegalArgumentException
     {
         if (baseURL == null)
         {
@@ -145,7 +139,7 @@ public class GMSClient
         }
         catch (MalformedURLException e)
         {
-            throw new IllegalArgumentException("URL is malformed: " + 
+            throw new IllegalArgumentException("URL is malformed: " +
                                                e.getMessage());
         }
 
@@ -169,6 +163,63 @@ public class GMSClient
         throw new UnsupportedOperationException("Not yet implemented");
     }
 
+
+
+    /**
+     * Obtain all of the users as userID - name in JSON format.
+     *
+     * @return List of HTTP Principal users.
+     * @throws IOException Any errors in reading.
+     */
+    public List<User<HttpPrincipal>> getDisplayUsers() throws IOException
+    {
+        final List<User<HttpPrincipal>> webUsers =
+                new ArrayList<User<HttpPrincipal>>();
+
+        final HttpDownload httpDownload =
+                createDisplayUsersHTTPDownload(webUsers);
+
+        httpDownload.setRequestProperty("Accept", "application/json");
+        httpDownload.run();
+
+        final Throwable error = httpDownload.getThrowable();
+
+        if (error != null)
+        {
+            final String errMessage = error.getMessage();
+            final int responseCode = httpDownload.getResponseCode();
+
+            log.debug("getDisplayUsers response " + responseCode + ": " +
+                      errMessage);
+
+            if ((responseCode == 401) || (responseCode == 403) ||
+                (responseCode == -1))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            else if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+
+            throw new IOException("HttpResponse (" + responseCode + ") - "
+                                  + errMessage);
+        }
+
+        log.debug("Content-Length: " + httpDownload.getContentLength());
+        log.debug("Content-Type: " + httpDownload.getContentType());
+
+        return webUsers;
+    }
+
+    HttpDownload createDisplayUsersHTTPDownload(
+            final List<User<HttpPrincipal>> webUsers) throws IOException
+    {
+        final URL usersListURL = new URL(this.baseURL + "/users");
+        return new HttpDownload(usersListURL,
+                                new JSONUserListInputStreamWrapper(webUsers));
+    }
+
     /**
      * Create a new group.
      *
@@ -176,17 +227,17 @@ public class GMSClient
      * @return The newly created group will all the information.
      * @throws GroupAlreadyExistsException If a group with the same name already
      *                                     exists.
-     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws AccessControlException      If unauthorized to perform this operation.
      * @throws UserNotFoundException
      * @throws IOException
      */
     public Group createGroup(Group group)
-        throws GroupAlreadyExistsException, AccessControlException, 
-               UserNotFoundException, IOException
+            throws GroupAlreadyExistsException, AccessControlException,
+                   UserNotFoundException, IOException
     {
         URL createGroupURL = new URL(this.baseURL + "/groups");
         log.debug("createGroupURL request to " + createGroupURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
@@ -207,8 +258,8 @@ public class GMSClient
         {
             log.debug("createGroup throwable", error);
             // transfer returns a -1 code for anonymous uploads.
-            if ((transfer.getResponseCode() == -1) || 
-                (transfer.getResponseCode() == 401) || 
+            if ((transfer.getResponseCode() == -1) ||
+                (transfer.getResponseCode() == 401) ||
                 (transfer.getResponseCode() == 403))
             {
                 throw new AccessControlException(error.getMessage());
@@ -251,7 +302,7 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public Group getGroup(String groupName)
-        throws GroupNotFoundException, AccessControlException, IOException
+            throws GroupNotFoundException, AccessControlException, IOException
     {
         URL getGroupURL = new URL(this.baseURL + "/groups/" + groupName);
         log.debug("getGroup request to " + getGroupURL.toString());
@@ -264,10 +315,11 @@ public class GMSClient
         Throwable error = transfer.getThrowable();
         if (error != null)
         {
-            log.debug("getGroup throwable (" + transfer.getResponseCode() + ")", error);
+            log.debug("getGroup throwable (" + transfer
+                    .getResponseCode() + ")", error);
             // transfer returns a -1 code for anonymous access.
-            if ((transfer.getResponseCode() == -1) || 
-                (transfer.getResponseCode() == 401) || 
+            if ((transfer.getResponseCode() == -1) ||
+                (transfer.getResponseCode() == 401) ||
                 (transfer.getResponseCode() == 403))
             {
                 throw new AccessControlException(error.getMessage());
@@ -295,7 +347,7 @@ public class GMSClient
             throw new RuntimeException(bug);
         }
     }
-    
+
     /**
      * Get the all group names.
      *
@@ -304,7 +356,7 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public List<String> getGroupNames()
-        throws AccessControlException, IOException
+            throws AccessControlException, IOException
     {
         final URL getGroupNamesURL = new URL(this.baseURL + "/groups");
         log.debug("getGroupNames request to " + getGroupNamesURL.toString());
@@ -312,26 +364,28 @@ public class GMSClient
         final List<String> groupNames = new ArrayList<String>();
         final HttpDownload httpDownload =
                 new HttpDownload(getGroupNamesURL, new InputStreamWrapper()
-        {
-            @Override
-            public void read(final InputStream inputStream) throws IOException
-            {
-                try
                 {
-                    InputStreamReader inReader = new InputStreamReader(inputStream);
-                    BufferedReader reader = new BufferedReader(inReader);
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        groupNames.add(line);
+                    @Override
+                    public void read(final InputStream inputStream) throws
+                                                                    IOException
+                    {
+                        try
+                        {
+                            InputStreamReader inReader = new InputStreamReader(inputStream);
+                            BufferedReader reader = new BufferedReader(inReader);
+                            String line;
+                            while ((line = reader.readLine()) != null)
+                            {
+                                groupNames.add(line);
+                            }
+                        }
+                        catch (Exception bug)
+                        {
+                            log.error("Unexpected exception", bug);
+                            throw new RuntimeException(bug);
+                        }
                     }
-                }
-                catch (Exception bug)
-                {
-                    log.error("Unexpected exception", bug);
-                    throw new RuntimeException(bug);
-                }
-            }
-        });
+                });
 
         httpDownload.setSSLSocketFactory(getSSLSocketFactory());
         httpDownload.run();
@@ -346,8 +400,8 @@ public class GMSClient
             log.debug("getGroupNames response " + responseCode + ": " +
                       errMessage);
 
-            if ((responseCode == 401) || (responseCode == 403) || 
-                    (responseCode == -1))
+            if ((responseCode == 401) || (responseCode == 403) ||
+                (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -370,36 +424,37 @@ public class GMSClient
      * @param group The update group object.
      * @return The group after update.
      * @throws IllegalArgumentException If cyclical membership is detected.
-     * @throws GroupNotFoundException If the group was not found.
-     * @throws UserNotFoundException If a member was not found.
-     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws GroupNotFoundException   If the group was not found.
+     * @throws UserNotFoundException    If a member was not found.
+     * @throws AccessControlException   If unauthorized to perform this operation.
      * @throws java.io.IOException
      */
     public Group updateGroup(Group group)
-        throws IllegalArgumentException, GroupNotFoundException, UserNotFoundException,
-               AccessControlException, IOException
+            throws IllegalArgumentException, GroupNotFoundException,
+                   UserNotFoundException,
+                   AccessControlException, IOException
     {
         URL updateGroupURL = new URL(this.baseURL + "/groups/" + group.getID());
         log.debug("updateGroup request to " + updateGroupURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
         StringBuilder groupXML = new StringBuilder();
         GroupWriter.write(group, groupXML);
         log.debug("updateGroup: " + groupXML);
-        
-        HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(), 
+
+        HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(),
                                          "application/xml", true);
         transfer.setSSLSocketFactory(getSSLSocketFactory());
         transfer.run();
-        
+
         Throwable error = transfer.getThrowable();
         if (error != null)
         {
             // transfer returns a -1 code for anonymous access.
-            if ((transfer.getResponseCode() == -1) || 
-                (transfer.getResponseCode() == 401) || 
+            if ((transfer.getResponseCode() == -1) ||
+                (transfer.getResponseCode() == 401) ||
                 (transfer.getResponseCode() == 403))
             {
                 throw new AccessControlException(error.getMessage());
@@ -410,14 +465,19 @@ public class GMSClient
             }
             if (transfer.getResponseCode() == 404)
             {
-                if (error.getMessage() != null && error.getMessage().toLowerCase().contains("user"))
+                if (error.getMessage() != null && error.getMessage()
+                        .toLowerCase().contains("user"))
+                {
                     throw new UserNotFoundException(error.getMessage());
+                }
                 else
+                {
                     throw new GroupNotFoundException(error.getMessage());
+                }
             }
             throw new IOException(error);
         }
-        
+
         try
         {
             String retXML = transfer.getResponseBody();
@@ -440,15 +500,15 @@ public class GMSClient
      * @throws java.io.IOException
      */
     public void deleteGroup(String groupName)
-        throws GroupNotFoundException, AccessControlException, IOException
+            throws GroupNotFoundException, AccessControlException, IOException
     {
         URL deleteGroupURL = new URL(this.baseURL + "/groups/" + groupName);
         log.debug("deleteGroup request to " + deleteGroupURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
-        HttpURLConnection conn = 
+        HttpURLConnection conn =
                 (HttpURLConnection) deleteGroupURL.openConnection();
         conn.setRequestMethod("DELETE");
 
@@ -465,19 +525,19 @@ public class GMSClient
         {
             responseCode = conn.getResponseCode();
         }
-        catch(Exception e)
+        catch (Exception e)
         {
             throw new AccessControlException(e.getMessage());
         }
-        
+
         if (responseCode != 200)
         {
             String errMessage = NetUtil.getErrorBody(conn);
-            log.debug("deleteGroup response " + responseCode + ": " + 
+            log.debug("deleteGroup response " + responseCode + ": " +
                       errMessage);
 
-            if ((responseCode == 401) || (responseCode == 403) || 
-                    (responseCode == -1))
+            if ((responseCode == 401) || (responseCode == 403) ||
+                (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -499,19 +559,19 @@ public class GMSClient
      * @param targetGroupName The group in which to add the group member.
      * @param groupMemberName The group member to add.
      * @throws IllegalArgumentException If cyclical membership is detected.
-     * @throws GroupNotFoundException If the group was not found.
-     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws GroupNotFoundException   If the group was not found.
+     * @throws AccessControlException   If unauthorized to perform this operation.
      * @throws java.io.IOException
      */
     public void addGroupMember(String targetGroupName, String groupMemberName)
-        throws IllegalArgumentException, GroupNotFoundException,
-               AccessControlException, IOException
+            throws IllegalArgumentException, GroupNotFoundException,
+                   AccessControlException, IOException
     {
-        URL addGroupMemberURL = new URL(this.baseURL + "/groups/" + 
-                                        targetGroupName + "/groupMembers/" + 
+        URL addGroupMemberURL = new URL(this.baseURL + "/groups/" +
+                                        targetGroupName + "/groupMembers/" +
                                         groupMemberName);
         log.debug("addGroupMember request to " + addGroupMemberURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
@@ -527,8 +587,8 @@ public class GMSClient
             final int responseCode = httpUpload.getResponseCode();
             final String errMessage = error.getMessage();
 
-            if ((responseCode == -1) || 
-                (responseCode == 401) || 
+            if ((responseCode == -1) ||
+                (responseCode == 401) ||
                 (responseCode == 403))
             {
                 throw new AccessControlException(errMessage);
@@ -549,23 +609,24 @@ public class GMSClient
      * Add a user as a member of a group.
      *
      * @param targetGroupName The group in which to add the group member.
-     * @param userID The user to add.
+     * @param userID          The user to add.
      * @throws GroupNotFoundException If the group was not found.
-     * @throws UserNotFoundException If the member was not found.
+     * @throws UserNotFoundException  If the member was not found.
      * @throws java.io.IOException
      * @throws AccessControlException If unauthorized to perform this operation.
      */
     public void addUserMember(String targetGroupName, Principal userID)
-        throws GroupNotFoundException, UserNotFoundException, AccessControlException, IOException
+            throws GroupNotFoundException, UserNotFoundException,
+                   AccessControlException, IOException
     {
         String userIDType = AuthenticationUtil.getPrincipalType(userID);
         String encodedUserID = URLEncoder.encode(userID.getName(), "UTF-8");
-        URL addUserMemberURL = new URL(this.baseURL + "/groups/" + 
-                                       targetGroupName + "/userMembers/" + 
+        URL addUserMemberURL = new URL(this.baseURL + "/groups/" +
+                                       targetGroupName + "/userMembers/" +
                                        encodedUserID + "?idType=" + userIDType);
 
         log.debug("addUserMember request to " + addUserMemberURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
@@ -581,8 +642,8 @@ public class GMSClient
             final int responseCode = httpUpload.getResponseCode();
             final String errMessage = error.getMessage();
 
-            if ((responseCode == -1) || 
-                (responseCode == 401) || 
+            if ((responseCode == -1) ||
+                (responseCode == 401) ||
                 (responseCode == 403))
             {
                 throw new AccessControlException(errMessage);
@@ -593,10 +654,15 @@ public class GMSClient
             }
             if (responseCode == 404)
             {
-                if (errMessage != null && errMessage.toLowerCase().contains("user"))
+                if (errMessage != null && errMessage.toLowerCase()
+                        .contains("user"))
+                {
                     throw new UserNotFoundException(errMessage);
+                }
                 else
+                {
                     throw new GroupNotFoundException(errMessage);
+                }
             }
             throw new IOException(errMessage);
         }
@@ -611,20 +677,20 @@ public class GMSClient
      * @throws java.io.IOException
      * @throws AccessControlException If unauthorized to perform this operation.
      */
-    public void removeGroupMember(String targetGroupName, 
+    public void removeGroupMember(String targetGroupName,
                                   String groupMemberName)
-        throws GroupNotFoundException, AccessControlException, IOException
+            throws GroupNotFoundException, AccessControlException, IOException
     {
-        URL removeGroupMemberURL = new URL(this.baseURL + "/groups/" + 
-                                           targetGroupName + "/groupMembers/" + 
+        URL removeGroupMemberURL = new URL(this.baseURL + "/groups/" +
+                                           targetGroupName + "/groupMembers/" +
                                            groupMemberName);
-        log.debug("removeGroupMember request to " + 
+        log.debug("removeGroupMember request to " +
                   removeGroupMemberURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
-        HttpURLConnection conn = 
+        HttpURLConnection conn =
                 (HttpURLConnection) removeGroupMemberURL.openConnection();
         conn.setRequestMethod("DELETE");
 
@@ -634,23 +700,25 @@ public class GMSClient
             ((HttpsURLConnection) conn)
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
-        
+
         // Try to handle anonymous access and throw AccessControlException 
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore) {}
-        
+        catch (Exception ignore)
+        {
+        }
+
         if (responseCode != 200)
         {
             String errMessage = NetUtil.getErrorBody(conn);
-            log.debug("removeGroupMember response " + responseCode + ": " + 
+            log.debug("removeGroupMember response " + responseCode + ": " +
                       errMessage);
 
-            if ((responseCode == -1) || 
-                (responseCode == 401) || 
+            if ((responseCode == -1) ||
+                (responseCode == 401) ||
                 (responseCode == 403))
             {
                 throw new AccessControlException(errMessage);
@@ -671,29 +739,30 @@ public class GMSClient
      * Remove a user as a member of a group.
      *
      * @param targetGroupName The group from which to remove the group member.
-     * @param userID The user to remove.
+     * @param userID          The user to remove.
      * @throws GroupNotFoundException If the group was not found.
-     * @throws UserNotFoundException If the member was not found.
+     * @throws UserNotFoundException  If the member was not found.
      * @throws java.io.IOException
      * @throws AccessControlException If unauthorized to perform this operation.
      */
     public void removeUserMember(String targetGroupName, Principal userID)
-        throws GroupNotFoundException, UserNotFoundException, AccessControlException, IOException
+            throws GroupNotFoundException, UserNotFoundException,
+                   AccessControlException, IOException
     {
         String userIDType = AuthenticationUtil.getPrincipalType(userID);
         String encodedUserID = URLEncoder.encode(userID.toString(), "UTF-8");
-        URL removeUserMemberURL = new URL(this.baseURL + "/groups/" + 
-                                          targetGroupName + "/userMembers/" + 
-                                          encodedUserID + "?idType=" + 
+        URL removeUserMemberURL = new URL(this.baseURL + "/groups/" +
+                                          targetGroupName + "/userMembers/" +
+                                          encodedUserID + "?idType=" +
                                           userIDType);
 
         log.debug("removeUserMember request to " +
                   removeUserMemberURL.toString());
-        
+
         // reset the state of the cache
         clearCache();
 
-        HttpURLConnection conn = 
+        HttpURLConnection conn =
                 (HttpURLConnection) removeUserMemberURL.openConnection();
         conn.setRequestMethod("DELETE");
 
@@ -703,23 +772,25 @@ public class GMSClient
             ((HttpsURLConnection) conn)
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
-        
+
         // Try to handle anonymous access and throw AccessControlException 
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore) {}
+        catch (Exception ignore)
+        {
+        }
 
         if (responseCode != 200)
         {
             String errMessage = NetUtil.getErrorBody(conn);
-            log.debug("removeUserMember response " + responseCode + ": " + 
+            log.debug("removeUserMember response " + responseCode + ": " +
                       errMessage);
 
-            if ((responseCode == -1) || 
-                (responseCode == 401) || 
+            if ((responseCode == -1) ||
+                (responseCode == 401) ||
                 (responseCode == 403))
             {
                 throw new AccessControlException(errMessage);
@@ -730,10 +801,15 @@ public class GMSClient
             }
             if (responseCode == 404)
             {
-                if (errMessage != null && errMessage.toLowerCase().contains("user"))
+                if (errMessage != null && errMessage.toLowerCase()
+                        .contains("user"))
+                {
                     throw new UserNotFoundException(errMessage);
+                }
                 else
+                {
                     throw new GroupNotFoundException(errMessage);
+                }
             }
             throw new IOException(errMessage);
         }
@@ -741,42 +817,42 @@ public class GMSClient
 
     /**
      * Get all the memberships of the user of a certain role.
-     * 
+     *
      * @param userID Identifies the user.
-     * @param role The role to look up.
+     * @param role   The role to look up.
      * @return A list of groups for which the user has the role.
-     * @throws UserNotFoundException If the user does not exist.
-     * @throws AccessControlException If not allowed to peform the search.
+     * @throws UserNotFoundException    If the user does not exist.
+     * @throws AccessControlException   If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException If an unknown error occured.
+     * @throws IOException              If an unknown error occured.
      */
     public List<Group> getMemberships(Principal userID, Role role)
-        throws UserNotFoundException, AccessControlException, IOException
+            throws UserNotFoundException, AccessControlException, IOException
     {
         if (userID == null || role == null)
         {
             throw new IllegalArgumentException("userID and role are required.");
         }
-        
+
         List<Group> cachedGroups = getCachedGroups(userID, role);
         if (cachedGroups != null)
         {
             return cachedGroups;
         }
-        
+
         String idType = AuthenticationUtil.getPrincipalType(userID);
         String id = userID.getName();
         String roleString = role.getValue();
-        
+
         StringBuilder searchGroupURL = new StringBuilder(this.baseURL);
         searchGroupURL.append("/search?");
-        
+
         searchGroupURL.append("ID=").append(URLEncoder.encode(id, "UTF-8"));
         searchGroupURL.append("&IDTYPE=")
                 .append(URLEncoder.encode(idType, "UTF-8"));
         searchGroupURL.append("&ROLE=")
                 .append(URLEncoder.encode(roleString, "UTF-8"));
-        
+
         log.debug("getMemberships request to " + searchGroupURL.toString());
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         URL url = new URL(searchGroupURL.toString());
@@ -790,8 +866,8 @@ public class GMSClient
         {
             log.debug("getMemberships throwable", error);
             // transfer returns a -1 code for anonymous access.
-            if ((transfer.getResponseCode() == -1) || 
-                (transfer.getResponseCode() == 401) || 
+            if ((transfer.getResponseCode() == -1) ||
+                (transfer.getResponseCode() == 401) ||
                 (transfer.getResponseCode() == 403))
             {
                 throw new AccessControlException(error.getMessage());
@@ -821,50 +897,50 @@ public class GMSClient
             throw new RuntimeException(bug);
         }
     }
-    
+
     /**
      * Return the group, specified by paramter groupName, if the user,
      * identified by userID, is a member of that group.  Return null
      * otherwise.
-     * 
+     * <p/>
      * This call is identical to getMemberShip(userID, groupName, Role.MEMBER)
-     *  
-     * @param userID Identifies the user.
+     *
+     * @param userID    Identifies the user.
      * @param groupName Identifies the group.
      * @return The group or null of the user is not a member.
-     * @throws UserNotFoundException If the user does not exist.
-     * @throws AccessControlException If not allowed to peform the search.
+     * @throws UserNotFoundException    If the user does not exist.
+     * @throws AccessControlException   If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException If an unknown error occured.
+     * @throws IOException              If an unknown error occured.
      */
     public Group getMembership(Principal userID, String groupName)
-        throws UserNotFoundException, AccessControlException, IOException
+            throws UserNotFoundException, AccessControlException, IOException
     {
         return getMembership(userID, groupName, Role.MEMBER);
     }
-    
+
     /**
      * Return the group, specified by paramter groupName, if the user,
      * identified by userID, is a member (of type role) of that group.
      * Return null otherwise.
-     * 
-     * @param userID Identifies the user.
+     *
+     * @param userID    Identifies the user.
      * @param groupName Identifies the group.
-     * @param role The membership role to search.
+     * @param role      The membership role to search.
      * @return The group or null of the user is not a member.
-     * @throws UserNotFoundException If the user does not exist.
-     * @throws AccessControlException If not allowed to peform the search.
+     * @throws UserNotFoundException    If the user does not exist.
+     * @throws AccessControlException   If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException If an unknown error occured.
+     * @throws IOException              If an unknown error occured.
      */
     public Group getMembership(Principal userID, String groupName, Role role)
-        throws UserNotFoundException, AccessControlException, IOException
+            throws UserNotFoundException, AccessControlException, IOException
     {
         if (userID == null || groupName == null || role == null)
         {
             throw new IllegalArgumentException("userID and role are required.");
         }
-        
+
         List<Group> cachedGroups = getCachedGroups(userID, role);
         if (cachedGroups != null)
         {
@@ -878,11 +954,11 @@ public class GMSClient
                 return null;
             }
         }
-        
+
         String idType = AuthenticationUtil.getPrincipalType(userID);
         String id = userID.getName();
         String roleString = role.getValue();
-        
+
         StringBuilder searchGroupURL = new StringBuilder(this.baseURL);
         searchGroupURL.append("/search?");
 
@@ -893,7 +969,7 @@ public class GMSClient
                 .append(URLEncoder.encode(roleString, "UTF-8"));
         searchGroupURL.append("&GROUPID=")
                 .append(URLEncoder.encode(groupName, "UTF-8"));
-        
+
         log.debug("getMembership request to " + searchGroupURL.toString());
         ByteArrayOutputStream out = new ByteArrayOutputStream();
         URL url = new URL(searchGroupURL.toString());
@@ -907,8 +983,8 @@ public class GMSClient
         {
             log.debug("getMembership throwable", error);
             // transfer returns a -1 code for anonymous access.
-            if ((transfer.getResponseCode() == -1) || 
-                (transfer.getResponseCode() == 401) || 
+            if ((transfer.getResponseCode() == -1) ||
+                (transfer.getResponseCode() == 401) ||
                 (transfer.getResponseCode() == 403))
             {
                 throw new AccessControlException(error.getMessage());
@@ -948,40 +1024,40 @@ public class GMSClient
             throw new RuntimeException(bug);
         }
     }
-    
+
     /**
      * Check if userID is a member of groupName.
-     * 
+     * <p/>
      * This is equivalent to isMember(userID, groupName, Role.MEMBER)
-     * 
-     * @param userID Identifies the user.
+     *
+     * @param userID    Identifies the user.
      * @param groupName Identifies the group.
      * @return True if the user is a member of the group
-     * @throws UserNotFoundException If the user does not exist.
-     * @throws AccessControlException If not allowed to peform the search.
+     * @throws UserNotFoundException    If the user does not exist.
+     * @throws AccessControlException   If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException If an unknown error occured.
+     * @throws IOException              If an unknown error occured.
      */
     public boolean isMember(Principal userID, String groupName)
-        throws UserNotFoundException, AccessControlException, IOException
+            throws UserNotFoundException, AccessControlException, IOException
     {
         return isMember(userID, groupName, Role.MEMBER);
     }
-    
+
     /**
      * Check if userID is a member (of type role) of groupName.
-     * 
-     * @param userID Identifies the user.
+     *
+     * @param userID    Identifies the user.
      * @param groupName Identifies the group.
-     * @param role The type of membership.
+     * @param role      The type of membership.
      * @return True if the user is a member of the group
-     * @throws UserNotFoundException If the user does not exist.
-     * @throws AccessControlException If not allowed to peform the search.
+     * @throws UserNotFoundException    If the user does not exist.
+     * @throws AccessControlException   If not allowed to peform the search.
      * @throws IllegalArgumentException If a parameter is null.
-     * @throws IOException If an unknown error occured.
+     * @throws IOException              If an unknown error occured.
      */
     public boolean isMember(Principal userID, String groupName, Role role)
-        throws UserNotFoundException, AccessControlException, IOException
+            throws UserNotFoundException, AccessControlException, IOException
     {
         Group group = getMembership(userID, groupName, role);
         return group != null;
@@ -993,24 +1069,27 @@ public class GMSClient
     public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory)
     {
         if (mySocketFactory != null)
+        {
             throw new IllegalStateException("Illegal use of GMSClient: "
-                    + "cannot set SSLSocketFactory after using one created from Subject");
+                                            + "cannot set SSLSocketFactory after using one created from Subject");
+        }
         this.sslSocketFactory = sslSocketFactory;
         clearCache();
     }
-    
+
     private int subjectHashCode = 0;
+
     private SSLSocketFactory getSSLSocketFactory()
     {
         AccessControlContext ac = AccessController.getContext();
         Subject s = Subject.getSubject(ac);
-        
+
         // no real Subject: can only use the one from setSSLSocketFactory
         if (s == null || s.getPrincipals().isEmpty())
         {
             return sslSocketFactory;
         }
-        
+
         // lazy init
         if (this.mySocketFactory == null)
         {
@@ -1022,18 +1101,21 @@ public class GMSClient
         {
             int c = s.hashCode();
             if (c != subjectHashCode)
-                throw new IllegalStateException("Illegal use of " 
-                        + this.getClass().getSimpleName()
-                        + ": subject change not supported for internal SSLSocketFactory");
+            {
+                throw new IllegalStateException("Illegal use of "
+                                                + this.getClass()
+                                                        .getSimpleName()
+                                                + ": subject change not supported for internal SSLSocketFactory");
+            }
         }
         return this.mySocketFactory;
     }
-    
+
     protected void clearCache()
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
-        
+
         if (subject != null)
         {
             log.debug("Clearing cache");
@@ -1045,16 +1127,18 @@ public class GMSClient
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
-        
+
         // only consult cache if the userID is of the calling subject
         if (userIsSubject(userID, subject))
         {
-            Set groupCredentialSet = subject.getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) && 
+            Set groupCredentialSet = subject
+                    .getPrivateCredentials(GroupMemberships.class);
+            if ((groupCredentialSet != null) &&
                 (groupCredentialSet.size() == 1))
             {
                 Iterator i = groupCredentialSet.iterator();
-                GroupMemberships groupMemberships = ((GroupMemberships) i.next());
+                GroupMemberships groupMemberships = ((GroupMemberships) i
+                        .next());
                 return groupMemberships.memberships.get(role);
             }
         }
@@ -1065,15 +1149,16 @@ public class GMSClient
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
-        
+
         // only save to cache if the userID is of the calling subject
         if (userIsSubject(userID, subject))
         {
             log.debug("Caching groups for " + userID + ", role " + role);
-            
+
             final GroupMemberships groupCredentials;
-            Set groupCredentialSet = subject.getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) && 
+            Set groupCredentialSet = subject
+                    .getPrivateCredentials(GroupMemberships.class);
+            if ((groupCredentialSet != null) &&
                 (groupCredentialSet.size() == 1))
             {
                 Iterator i = groupCredentialSet.iterator();
@@ -1084,18 +1169,18 @@ public class GMSClient
                 groupCredentials = new GroupMemberships();
                 subject.getPrivateCredentials().add(groupCredentials);
             }
-            
-            groupCredentials.memberships.put(role,  groups);
+
+            groupCredentials.memberships.put(role, groups);
         }
     }
-    
+
     protected boolean userIsSubject(Principal userID, Subject subject)
     {
         if (userID == null || subject == null)
         {
             return false;
         }
-        
+
         for (Principal subjectPrincipal : subject.getPrincipals())
         {
             if (subjectPrincipal.equals(userID))
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e3a47447965467f2f822dc5a532fff1ab0571a0
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapper.java
@@ -0,0 +1,154 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.client;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.net.InputStreamWrapper;
+import ca.nrc.cadc.util.StringUtil;
+import org.apache.log4j.Logger;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.List;
+
+public class JSONUserListInputStreamWrapper implements InputStreamWrapper
+{
+    private static final Logger LOGGER = Logger
+            .getLogger(JSONUserListInputStreamWrapper.class);
+    private final List<User<HttpPrincipal>> output;
+
+
+    public JSONUserListInputStreamWrapper(
+            final List<User<HttpPrincipal>> output)
+    {
+        this.output = output;
+    }
+
+
+    /**
+     * Read the stream in.
+     *
+     * @param inputStream The stream to read from.
+     * @throws IOException Any reading exceptions.
+     */
+    @Override
+    public void read(final InputStream inputStream) throws IOException
+    {
+        String line = null;
+
+        try
+        {
+            final InputStreamReader inReader =
+                    new InputStreamReader(inputStream);
+            final BufferedReader reader = new BufferedReader(inReader);
+
+            while (StringUtil.hasText(line = reader.readLine()))
+            {
+                // Deal with arrays stuff.
+                while (line.startsWith("[") || line.startsWith(","))
+                {
+                    line = line.substring(1);
+                }
+
+                while (line.endsWith("]") || line.endsWith(","))
+                {
+                    line = line.substring(0, (line.length() - 1));
+                }
+
+                if (StringUtil.hasText(line))
+                {
+                    LOGGER.debug(String.format("Reading: %s", line));
+
+                    final JSONObject jsonObject = new JSONObject(line);
+                    final User<HttpPrincipal> webUser =
+                            new User<HttpPrincipal>(
+                                    new HttpPrincipal(jsonObject
+                                                              .getString("id")));
+                    final String firstName = jsonObject.getString("firstName");
+                    final String lastName = jsonObject.getString("lastName");
+
+                    webUser.details
+                            .add(new PersonalDetails(firstName, lastName));
+
+                    output.add(webUser);
+                }
+            }
+        }
+        catch (Exception bug)
+        {
+            throw new IOException(bug + (StringUtil.hasText(line)
+                                         ? "Error line is " + line : ""));
+        }
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java
index 230559285f8a2349351a475015873035b39cc6f7..308148bcdadde1530c994bc1bea70a7a61f9cd14 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/json/UsersWriter.java
@@ -68,6 +68,7 @@
 
 package ca.nrc.cadc.ac.json;
 
+import ca.nrc.cadc.ac.PersonalDetails;
 import org.json.JSONException;
 import org.json.JSONWriter;
 
@@ -81,7 +82,7 @@ import java.util.Map;
  */
 public class UsersWriter
 {
-    public static void write(final Map<String, String> users,
+    public static void write(final Map<String, PersonalDetails> users,
                              final Writer writer) throws IOException
     {
         final JSONWriter jsonWriter = new JSONWriter(writer);
@@ -90,14 +91,19 @@ public class UsersWriter
         {
             jsonWriter.array();
 
-            for (final Map.Entry<String, String> entry : users.entrySet())
+            for (final Map.Entry<String, PersonalDetails> entry
+                    : users.entrySet())
             {
                 jsonWriter.object();
 
                 jsonWriter.key("id").value(entry.getKey());
-                jsonWriter.key("name").value(entry.getValue());
+                jsonWriter.key("firstName").value(entry.getValue().
+                        getFirstName());
+                jsonWriter.key("lastName").value(entry.getValue().
+                        getLastName());
 
                 jsonWriter.endObject();
+                writer.write("\n");
             }
         }
         catch (JSONException e)
@@ -112,7 +118,7 @@ public class UsersWriter
             }
             catch (JSONException e)
             {
-                throw new IOException(e);
+                // Do nothing.
             }
         }
     }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java
index f8584a468b14f2b23bef82632017f2192cf9f115..757af4901c0a2572ac563350ca76e142dd7d12ea 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/xml/UsersWriter.java
@@ -68,6 +68,7 @@
 
 package ca.nrc.cadc.ac.xml;
 
+import ca.nrc.cadc.ac.PersonalDetails;
 import org.jdom2.Document;
 import org.jdom2.Element;
 import org.jdom2.output.Format;
@@ -86,21 +87,25 @@ public class UsersWriter
      * @param writer            The Writer to output to.
      * @throws IOException      Any writing errors.
      */
-    public static void write(final Map<String, String> users,
+    public static void write(final Map<String, PersonalDetails> users,
                              final Writer writer) throws IOException
     {
         // Create the root users Element.
         final Element usersElement = new Element("users");
 
-        for (final Map.Entry<String, String> entry : users.entrySet())
+        for (final Map.Entry<String, PersonalDetails> entry : users.entrySet())
         {
             final Element userEntryElement = new Element("user");
-            final Element nameElement = new Element("name");
+            final Element firstNameElement = new Element("firstName");
+            final Element lastNameElement = new Element("lastName");
 
             userEntryElement.setAttribute("id", entry.getKey());
 
-            nameElement.setText(entry.getValue());
-            userEntryElement.addContent(nameElement);
+            firstNameElement.setText(entry.getValue().getFirstName());
+            userEntryElement.addContent(firstNameElement);
+
+            lastNameElement.setText(entry.getValue().getLastName());
+            userEntryElement.addContent(lastNameElement);
 
             usersElement.addContent(userEntryElement);
         }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
index 3025fb37678ac4fc3d0f464e6b320f9ced4ebcaf..2da9b1e8948109ca172b198bf0e96965b54b6a7e 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
@@ -69,6 +69,7 @@
 
 package ca.nrc.cadc.ac.client;
 
+import java.io.IOException;
 import java.net.URI;
 import java.net.URL;
 import java.security.PrivilegedExceptionAction;
@@ -77,10 +78,10 @@ import java.util.List;
 
 import javax.security.auth.Subject;
 
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.net.HttpDownload;
 import org.apache.log4j.Level;
 import org.apache.log4j.Logger;
-import org.junit.Assert;
-import org.junit.Test;
 
 import ca.nrc.cadc.ac.AC;
 import ca.nrc.cadc.ac.Group;
@@ -89,6 +90,11 @@ import ca.nrc.cadc.auth.HttpPrincipal;
 import ca.nrc.cadc.reg.client.RegistryClient;
 import ca.nrc.cadc.util.Log4jInit;
 
+import org.junit.Assert;
+import org.junit.Test;
+import static org.easymock.EasyMock.*;
+
+
 public class GMSClientTest
 {
     
@@ -98,7 +104,41 @@ public class GMSClientTest
     {
         Log4jInit.setLevel("ca.nrc.cadc.ac", Level.DEBUG);
     }
-    
+
+
+    @Test
+    public void testGetDisplayUsers() throws Exception
+    {
+        final HttpDownload mockHTTPDownload = createMock(HttpDownload.class);
+        final GMSClient testSubject = new GMSClient("http://mysite.com/users")
+        {
+            @Override
+            HttpDownload createDisplayUsersHTTPDownload(
+                    List<User<HttpPrincipal>> webUsers) throws IOException
+            {
+                return mockHTTPDownload;
+            }
+        };
+
+        mockHTTPDownload.setRequestProperty("Accept", "application/json");
+        expectLastCall().once();
+
+        mockHTTPDownload.run();
+        expectLastCall().once();
+
+        expect(mockHTTPDownload.getThrowable()).andReturn(null).once();
+
+        expect(mockHTTPDownload.getContentLength()).andReturn(88l).once();
+        expect(mockHTTPDownload.getContentType()).andReturn(
+                "application/json").once();
+
+        replay(mockHTTPDownload);
+
+        testSubject.getDisplayUsers();
+
+        verify(mockHTTPDownload);
+    }
+
     @Test
     public void testUserIsSubject()
     {
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fe977e4e3e5120c17b8e407df7bc210767384099
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/JSONUserListInputStreamWrapperTest.java
@@ -0,0 +1,102 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2015.                            (c) 2015.
+ *  Government of Canada                 Gouvernement du Canada
+ *  National Research Council            Conseil national de recherches
+ *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
+ *  All rights reserved                  Tous droits réservés
+ *
+ *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
+ *  expressed, implied, or               énoncée, implicite ou légale,
+ *  statutory, of any kind with          de quelque nature que ce
+ *  respect to the software,             soit, concernant le logiciel,
+ *  including without limitation         y compris sans restriction
+ *  any warranty of merchantability      toute garantie de valeur
+ *  or fitness for a particular          marchande ou de pertinence
+ *  purpose. NRC shall not be            pour un usage particulier.
+ *  liable in any event for any          Le CNRC ne pourra en aucun cas
+ *  damages, whether direct or           être tenu responsable de tout
+ *  indirect, special or general,        dommage, direct ou indirect,
+ *  consequential or incidental,         particulier ou général,
+ *  arising from the use of the          accessoire ou fortuit, résultant
+ *  software.  Neither the name          de l'utilisation du logiciel. Ni
+ *  of the National Research             le nom du Conseil National de
+ *  Council of Canada nor the            Recherches du Canada ni les noms
+ *  names of its contributors may        de ses  participants ne peuvent
+ *  be used to endorse or promote        être utilisés pour approuver ou
+ *  products derived from this           promouvoir les produits dérivés
+ *  software without specific prior      de ce logiciel sans autorisation
+ *  written permission.                  préalable et particulière
+ *                                       par écrit.
+ *
+ *  This file is part of the             Ce fichier fait partie du projet
+ *  OpenCADC project.                    OpenCADC.
+ *
+ *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
+ *  you can redistribute it and/or       vous pouvez le redistribuer ou le
+ *  modify it under the terms of         modifier suivant les termes de
+ *  the GNU Affero General Public        la “GNU Affero General Public
+ *  License as published by the          License” telle que publiée
+ *  Free Software Foundation,            par la Free Software Foundation
+ *  either version 3 of the              : soit la version 3 de cette
+ *  License, or (at your option)         licence, soit (à votre gré)
+ *  any later version.                   toute version ultérieure.
+ *
+ *  OpenCADC is distributed in the       OpenCADC est distribué
+ *  hope that it will be useful,         dans l’espoir qu’il vous
+ *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
+ *  without even the implied             GARANTIE : sans même la garantie
+ *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
+ *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
+ *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
+ *  General Public License for           Générale Publique GNU Affero
+ *  more details.                        pour plus de détails.
+ *
+ *  You should have received             Vous devriez avoir reçu une
+ *  a copy of the GNU Affero             copie de la Licence Générale
+ *  General Public License along         Publique GNU Affero avec
+ *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
+ *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
+ *                                       <http://www.gnu.org/licenses/>.
+ *
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.client;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.auth.HttpPrincipal;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+
+public class JSONUserListInputStreamWrapperTest
+{
+    @Test
+    public void readInputStream() throws Exception
+    {
+        final List<User<HttpPrincipal>> output =
+                new ArrayList<User<HttpPrincipal>>();
+        final JSONUserListInputStreamWrapper testSubject =
+                new JSONUserListInputStreamWrapper(output);
+        final InputStream inputStream =
+                new ByteArrayInputStream("[{\"id\":\"CADCTest\",\"firstName\":\"CADCtest\",\"lastName\":\"USER\"}\n,{\"id\":\"User_2\",\"firstName\":\"User\",\"lastName\":\"2\"}]".getBytes());
+
+        testSubject.read(inputStream);
+
+        assertEquals("First item is wrong.", "CADCTest",
+                     output.get(0).getUserID().getName());
+        assertEquals("First item is wrong.", "User_2",
+                     output.get(1).getUserID().getName());
+    }
+}