diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java
new file mode 100644
index 0000000000000000000000000000000000000000..6874d550ed26406ebbaae8cd9489f1a82b23f11a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupDetailSelector.java
@@ -0,0 +1,89 @@
+/*
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2011.                            (c) 2011.
+*  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/>.
+*
+*  $Revision: 5 $
+*
+************************************************************************
+*/
+
+package ca.nrc.cadc.ac.server;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.Role;
+
+/**
+ *
+ * @author pdowler
+ */
+public interface GroupDetailSelector 
+{
+    /** 
+     * Check if group details should be filled in for the group when
+     * querying for role.
+     * @param g
+     * @param r
+     * @return true if group details should be filled
+     */
+    boolean isDetailedSearch(Group g, Role r);
+}
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 625901a852d76ff0edd5087ba63f58b5711b3adc..d4049e2fb3363bce96b81fff1ae46b3366cff508 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
@@ -94,7 +94,7 @@ import java.util.Set;
 public abstract class LdapDAO
 {
 	private static final Logger logger = Logger.getLogger(LdapDAO.class);
-	
+
     private LDAPConnection conn;
 
     LdapConfig config;
@@ -234,7 +234,11 @@ public abstract class LdapDAO
             throws TransientException
     {
     	logger.debug("Ldap result: " + code);
-        System.out.println("Ldap result: " + code);
+
+    	if (code == ResultCode.SUCCESS || code == ResultCode.NO_SUCH_OBJECT)
+        {
+            return;
+        }
 
         if (code == ResultCode.INSUFFICIENT_ACCESS_RIGHTS)
         {
@@ -244,10 +248,6 @@ public abstract class LdapDAO
         {
             throw new AccessControlException("Invalid credentials ");
         }
-        else if ((code == ResultCode.SUCCESS) || (code == ResultCode.NO_SUCH_OBJECT))
-        {
-            // all good. nothing to do
-        }
         else if (code == ResultCode.PARAM_ERROR)
         {
             throw new IllegalArgumentException("Error in Ldap parameters ");
@@ -256,10 +256,12 @@ public abstract class LdapDAO
         {
             throw new TransientException("Connection problems ");
         }
-        else
+        else if (code == ResultCode.TIMEOUT || code == ResultCode.TIME_LIMIT_EXCEEDED)
         {
-            throw new RuntimeException("Ldap error (" + code.getName() + ")");
+            throw new TransientException("ldap timeout");
         }
+
+        throw new RuntimeException("Ldap error (" + code.getName() + ")");
     }
 
 }
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 71ddf32316e6de250ab5ef5b9a9c2afdb26657b6..1cee54aed419269958d481b45a0614ffb1180692 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,6 +68,18 @@
  */
 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;
@@ -75,21 +87,44 @@ import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.User;
 import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupDetailSelector;
 import ca.nrc.cadc.net.TransientException;
 import ca.nrc.cadc.util.StringUtil;
-import com.unboundid.ldap.sdk.*;
-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.*;
+import com.unboundid.ldap.sdk.AddRequest;
+import com.unboundid.ldap.sdk.Attribute;
+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.controls.ProxiedAuthorizationV2RequestControl;
 
 public class LdapGroupDAO<T extends Principal> extends LdapDAO
 {
     private static final Logger logger = Logger.getLogger(LdapGroupDAO.class);
-    
+
+    private static final String[] PUB_GROUP_ATTRS = new String[]
+    {
+        "entrydn", "cn"
+    };
+    private static final String[] GROUP_ATTRS = new String[]
+    {
+        "entrydn", "cn", "nsaccountlock", "owner", "modifytimestamp", "description"
+    };
+    private static final String[] GROUP_AND_MEMBER_ATTRS = new String[]
+    {
+        "entrydn", "cn", "nsaccountlock", "owner", "modifytimestamp", "description", "uniquemember"
+    };
+
     private LdapUserDAO<T> userPersist;
 
     public LdapGroupDAO(LdapConfig config, LdapUserDAO<T> userPersist)
@@ -105,33 +140,33 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
 
     /**
      * Persists a group.
-     * 
+     *
      * @param group The group to create
-     * 
+     *
      * @return created group
-     * 
-     * @throws GroupAlreadyExistsException If a group with the same ID already 
+     *
+     * @throws GroupAlreadyExistsException If a group with the same ID already
      *                                     exists.
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws UserNotFoundException If owner or a member not valid user.
-     * @throws GroupNotFoundException 
+     * @throws GroupNotFoundException
      */
     public Group addGroup(final Group group)
         throws GroupAlreadyExistsException, TransientException,
-               UserNotFoundException, AccessControlException, 
+               UserNotFoundException, AccessControlException,
                GroupNotFoundException
     {
         if (group.getOwner() == null)
         {
             throw new IllegalArgumentException("Group owner must be specified");
         }
-        
+
         if (!group.getProperties().isEmpty())
         {
             throw new UnsupportedOperationException(
                     "Support for groups properties not available");
         }
-        
+
         if (!isCreatorOwner(group.getOwner()))
         {
             throw new AccessControlException("Group owner must be creator");
@@ -146,22 +181,22 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             }
             else
             {
-                
+
                 DN ownerDN = userPersist.getUserDN(group.getOwner());
-                
+
                 // add group to groups tree
-                LDAPResult result = addGroup(getGroupDN(group.getID()), 
-                                             group.getID(), ownerDN, 
-                                             group.description, 
-                                             group.getUserMembers(), 
+                LDAPResult result = addGroup(getGroupDN(group.getID()),
+                                             group.getID(), ownerDN,
+                                             group.description,
+                                             group.getUserMembers(),
                                              group.getGroupMembers());
                 LdapDAO.checkLdapResult(result.getResultCode());
-                
+
                 // add group to admin groups tree
-                result = addGroup(getAdminGroupDN(group.getID()), 
-                                  group.getID(), ownerDN, 
-                                  group.description, 
-                                  group.getUserAdmins(), 
+                result = addGroup(getAdminGroupDN(group.getID()),
+                                  group.getID(), ownerDN,
+                                  group.description,
+                                  group.getUserAdmins(),
                                   group.getGroupAdmins());
                 LdapDAO.checkLdapResult(result.getResultCode());
                 // AD: Search results sometimes come incomplete if
@@ -182,24 +217,24 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         	logger.debug("addGroup Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
             throw new RuntimeException("Unexpected LDAP exception", e);
-        } 
+        }
     }
-    
+
     private LDAPResult addGroup(final DN groupDN, final String groupID,
-                                final DN ownerDN, final String description, 
-                                final Set<User<? extends Principal>> users, 
+                                final DN ownerDN, final String description,
+                                final Set<User<? extends Principal>> users,
                                 final Set<Group> groups)
-        throws UserNotFoundException, LDAPException, TransientException, 
+        throws UserNotFoundException, LDAPException, TransientException,
         AccessControlException, GroupNotFoundException
     {
         // add new group
         List<Attribute> attributes = new ArrayList<Attribute>();
-        Attribute ownerAttribute = 
+        Attribute ownerAttribute =
                         new Attribute("owner", ownerDN.toNormalizedString());
         attributes.add(ownerAttribute);
         attributes.add(new Attribute("objectClass", "groupofuniquenames"));
         attributes.add(new Attribute("cn", groupID));
-        
+
         if (StringUtil.hasText(description))
         {
             attributes.add(new Attribute("description", description));
@@ -223,7 +258,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         if (!members.isEmpty())
         {
-            attributes.add(new Attribute("uniquemember", 
+            attributes.add(new Attribute("uniquemember",
                 (String[]) members.toArray(new String[members.size()])));
         }
 
@@ -232,10 +267,11 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
                 new ProxiedAuthorizationV2RequestControl(
                         "dn:" + getSubjectDN().toNormalizedString()));
 
+        logger.debug("addGroup: " + groupDN);
         return getConnection().add(addRequest);
     }
-    
-    
+
+
     /**
      * Checks whether group name available for the user or already in use.
      * @param group
@@ -244,7 +280,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
      * @throws UserNotFoundException
      * @throws GroupNotFoundException
      * @throws TransientException
-     * @throws GroupAlreadyExistsException 
+     * @throws GroupAlreadyExistsException
      */
     private Group reactivateGroup(final Group group)
         throws AccessControlException, UserNotFoundException,
@@ -252,22 +288,20 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
     {
         try
         {
-            // check group name exists           
+            // check group name exists
             Filter filter = Filter.createEqualityFilter("cn", group.getID());
 
-            SearchRequest searchRequest = 
-                    new SearchRequest(
-                            getGroupDN(group.getID())
-                            .toNormalizedString(), SearchScope.SUB, filter, 
+            DN groupDN = getGroupDN(group.getID());
+            SearchRequest searchRequest =
+                    new SearchRequest(groupDN.toNormalizedString(), SearchScope.BASE, filter,
                                       new String[] {"nsaccountlock"});
 
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
 
-            SearchResultEntry searchResult = 
-                    getConnection().searchForEntry(searchRequest);
-            
+            SearchResultEntry searchResult = getConnection().searchForEntry(searchRequest);
+
             if (searchResult == null)
             {
                 return null;
@@ -277,18 +311,18 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             {
                 throw new GroupAlreadyExistsException("Group already exists " + group.getID());
             }
-            
-            // activate group            
+
+            // activate group
             try
             {
                 return modifyGroup(null, group, true);
-            } 
+            }
             catch (GroupNotFoundException e)
             {
                 throw new RuntimeException(
                         "BUG: group to modify does not exist" + group.getID());
-            }          
-        } 
+            }
+        }
         catch (LDAPException e)
         {
         	logger.debug("reactivateGroup Exception: " + e, e);
@@ -296,67 +330,72 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new RuntimeException("Unexpected LDAP exception", e);
         }
     }
-    
-    
+
+
     /**
      * Get all group names.
-     * 
+     *
      * @return A collection of strings
-     * 
+     *
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
-    public Collection<String> getGroupNames() throws TransientException
+    public Collection<String> getGroupNames()
+        throws TransientException
     {
         try
         {
-            final Filter filter = Filter.createPresenceFilter("cn");
-            final String [] attributes = new String[] {"cn", "nsaccountlock"};
-            final Collection<String> groupNames = new ArrayList<String>();
-            final long begin = System.currentTimeMillis();
+            Filter filter = Filter.createPresenceFilter("cn");
+            String [] attributes = new String[] {"cn", "nsaccountlock"};
+
+            SearchRequest searchRequest =
+                    new SearchRequest(config.getGroupsDN(),
+                                      SearchScope.SUB, filter, attributes);
 
-            final SearchResult searchResult =
-                    getConnection().search(new SearchResultListener()
+            SearchResult searchResult = null;
+            try
+            {
+                searchResult = getConnection().search(searchRequest);
+            }
+            catch (LDAPSearchException e)
             {
-                @Override
-                public void searchEntryReturned(
-                        final SearchResultEntry searchEntry)
+                logger.debug("Could not find groups root", e);
+                LdapDAO.checkLdapResult(e.getResultCode());
+                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
                 {
-                    groupNames.add(searchEntry.getAttributeValue("cn"));
+                    throw new IllegalStateException("Could not find groups root");
                 }
 
-                @Override
-                public void searchReferenceReturned(
-                        final SearchResultReference searchReference)
-                {
-
-                }
-            }, config.getGroupsDN(), SearchScope.ONE, filter, attributes);
+                throw new IllegalStateException("unexpected failure", e);
+            }
 
             LdapDAO.checkLdapResult(searchResult.getResultCode());
-            long end = System.currentTimeMillis();
+            List<String> groupNames = new ArrayList<String>();
+            for (SearchResultEntry next : searchResult.getSearchEntries())
+            {
+                if (!next.hasAttribute("nsaccountlock"))
+                {
+                    groupNames.add(next.getAttributeValue("cn"));
+                }
+            }
 
-            logger.info("<-- groupNames in " + ((new Long(end).doubleValue()
-                                                 - new Long(begin).doubleValue())
-                                                / 1000.0) + " seconds.");
             return groupNames;
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getGroupNames Exception: " + e1, e1);
+            logger.debug("getGroupNames Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
-            throw new IllegalStateException("Unexpected exception: "
-                                            + e1.getMatchedDN(), e1);
+            throw new IllegalStateException("Unexpected exception: " + e1.getMatchedDN(), e1);
         }
-        
+
     }
 
     /**
-     * Get the group with the given Group ID.
-     * 
+     * Get the group with members.
+     *
      * @param groupID The Group unique ID.
-     * 
+     *
      * @return A Group instance
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException  If an temporary, unexpected problem occurred.
      */
@@ -364,185 +403,96 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         throws GroupNotFoundException, TransientException,
                AccessControlException
     {
-        return getGroup(groupID, true);
-    }
-    
-    public Group getGroup(final String groupID, final boolean withMembers)
-        throws GroupNotFoundException, TransientException,
-               AccessControlException
-    {
-        Group group = getGroup(getGroupDN(groupID), groupID, true);
-        
-        Group adminGroup = getAdminGroup(getAdminGroupDN(groupID), groupID, 
-                                         true);
-        
+        Group group = getGroup(getGroupDN(groupID), groupID, GROUP_AND_MEMBER_ATTRS);
+
+        Group adminGroup = getGroup(getAdminGroupDN(groupID), null, GROUP_AND_MEMBER_ATTRS);
+
         group.getGroupAdmins().addAll(adminGroup.getGroupMembers());
         group.getUserAdmins().addAll(adminGroup.getUserMembers());
+
         return group;
     }
-    
-    private Group getGroup(final DN groupDN, final String groupID, 
-                           final boolean withMembers)
-        throws GroupNotFoundException, TransientException, 
-               AccessControlException
-    {
-        String [] attributes = new String[] {"entrydn", "cn", "description", 
-                                             "owner", "uniquemember", 
-                                             "modifytimestamp", "nsaccountlock"};
-        return getGroup(groupDN, groupID, withMembers, attributes);
-    }
-    
-    private Group getAdminGroup(final DN groupDN, final String groupID, 
-                                final boolean withMembers)
-        throws GroupNotFoundException, TransientException, 
-               AccessControlException
-    {
-        String [] attributes = new String[] {"entrydn", "cn", "owner",
-                                             "uniquemember"};
-        return getGroup(groupDN, groupID, withMembers, attributes);
-    }
 
-    private Group getGroup(final DN groupDN, final String groupID, 
-                           final boolean withMembers, final String[] attributes)
-        throws GroupNotFoundException, TransientException, 
+    // groupID is here so exceptions and loggiong have plain groupID instead of DN
+    private Group getGroup(final DN groupDN, final String xgroupID, String[] attributes)
+        throws GroupNotFoundException, TransientException,
                AccessControlException
     {
+        logger.debug("getGroup: " + groupDN + " attrs: " + attributes.length);
+        String loggableGroupID = xgroupID;
+        if (loggableGroupID == null)
+            loggableGroupID = groupDN.toString(); // member or admin group: same name, internal tree
+
         try
         {
-            Filter filter = Filter.createEqualityFilter("cn", groupID);
-            
-            SearchRequest searchRequest = 
-                    new SearchRequest(groupDN.toNormalizedString(), 
-                                      SearchScope.SUB, filter, attributes);
+            Filter filter = Filter.createNOTFilter(Filter.createPresenceFilter("nsaccountlock"));
+
+            SearchRequest searchRequest =
+                    new SearchRequest(groupDN.toNormalizedString(),
+                                      SearchScope.BASE, filter, attributes);
 
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
 
-            SearchResult searchResult = null;
-            try
-            {
-                searchResult = getConnection().search(searchRequest);
-            }
-            catch (LDAPSearchException e)
-            {
-                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
-                {
-                    String msg = "Group not found " + groupID;
-                    logger.debug(msg);
-                    throw new GroupNotFoundException(groupID);
-                }
-                else
-                {
-                    LdapDAO.checkLdapResult(e.getResultCode());
-                }
-            }
-            
-            if (searchResult.getEntryCount() == 0)
-            {
-                LdapDAO.checkLdapResult(searchResult.getResultCode());
-                //access denied
-                String msg = "Not authorized to access " + groupID;
-                logger.debug(msg);
-                throw new AccessControlException(groupID);
-            }
-            
-            if (searchResult.getEntryCount() >1)
-            {
-                throw new RuntimeException("BUG: multiple results when retrieving group " + groupID);
-            }
-            
-            SearchResultEntry searchEntry = searchResult.getSearchEntries().get(0);
-            
-            if (searchEntry.getAttribute("nsaccountlock") != null)
-            {
-                // deleted group
-                String msg = "Group not found " + groupID;
-                logger.debug(msg);
-                throw new GroupNotFoundException(groupID);
-            }
-            
-            DN groupOwner = searchEntry.getAttributeValueAsDN("owner");
-            if (groupOwner == null)
-            {
-                //TODO assume user not allowed to read group
-                throw new AccessControlException(groupID);
-            }
-            
-            User<X500Principal> owner;
-            try
-            {
-                owner = userPersist.getX500User(groupOwner);
-            }
-            catch (UserNotFoundException e)
-            {
-                throw new RuntimeException("BUG: group owner not found");
-            }
-            
-            Group ldapGroup = new Group(groupID, owner);
-            if (searchEntry.hasAttribute("description"))
-            {
-                ldapGroup.description = 
-                        searchEntry.getAttributeValue("description");
-            }
-            if (searchEntry.hasAttribute("modifytimestamp"))
+
+            SearchResultEntry searchEntry = getConnection().searchForEntry(searchRequest);
+
+            if (searchEntry == null)
             {
-                ldapGroup.lastModified = 
-                        searchEntry.getAttributeValueAsDate("modifytimestamp");
+                String msg = "Group not found " + loggableGroupID;
+                logger.debug(msg + " cause: null");
+                throw new GroupNotFoundException(loggableGroupID);
             }
 
-            if (withMembers)
+            Group ldapGroup = createGroupFromEntry(searchEntry, attributes);
+
+            if (searchEntry.getAttributeValues("uniquemember") != null)
             {
-                if (searchEntry.getAttributeValues("uniquemember") != null)
+                for (String member : searchEntry.getAttributeValues("uniquemember"))
                 {
-                    for (String member : searchEntry
-                            .getAttributeValues("uniquemember"))
+                    DN memberDN = new DN(member);
+                    if (memberDN.isDescendantOf(config.getUsersDN(), false))
                     {
-                        DN memberDN = new DN(member);
-                        if (memberDN.isDescendantOf(config.getUsersDN(), false))
+                        User<X500Principal> user;
+                        try
                         {
-                            User<X500Principal> user;
-                            try
-                            {
-                                user = userPersist.getX500User(memberDN);
-                            }
-                            catch (UserNotFoundException e)
-                            {
-                                throw new RuntimeException(
-                                    "BUG: group member not found");
-                            }
+                            user = userPersist.getX500User(memberDN);
                             ldapGroup.getUserMembers().add(user);
                         }
-                        else if (memberDN.isDescendantOf(config.getGroupsDN(),
-                                                         false))
+                        catch (UserNotFoundException e)
                         {
-                            try
-                            {
-                                ldapGroup.getGroupMembers().
-                                    add(new Group(getGroupID(memberDN)));
-                            }
-                            catch(GroupNotFoundException e)
-                            {
-                                // ignore as we are not cleaning up
-                                // deleted groups from the group members
-                            }
+                            // ignore as we do not cleanup deleted users
+                            // from groups they belong to
                         }
-                        else
+                    }
+                    else if (memberDN.isDescendantOf(config.getGroupsDN(), false))
+                    {
+                        try
+                        {
+                            ldapGroup.getGroupMembers().add(getGroup(memberDN, null, PUB_GROUP_ATTRS));
+                        }
+                        catch(GroupNotFoundException e)
                         {
-                            throw new RuntimeException(
-                                "BUG: unknown member DN type: " + memberDN);
+                            // ignore as we are not cleaning up
+                            // deleted groups from the group members
                         }
                     }
+                    else
+                    {
+                        throw new RuntimeException(
+                            "BUG: unknown member DN type: " + memberDN);
+                    }
                 }
             }
-            
+
             return ldapGroup;
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getGroup Exception: " + e1, e1);
+            logger.debug("getGroup Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
-            throw new GroupNotFoundException("Not found " + groupID);
+            throw new RuntimeException("BUG: checkLdapResult didn't throw an exception");
         }
     }
 
@@ -550,9 +500,9 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
      * Modify the given group.
      *
      * @param group The group to update. It must be an existing group
-     * 
+     *
      * @return The newly updated group.
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException If an temporary, unexpected problem occurred.
      * @throws AccessControlException If the operation is not permitted.
@@ -563,9 +513,9 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
                AccessControlException, UserNotFoundException
     {
         Group existing = getGroup(group.getID()); //group must exists first
-        return modifyGroup(existing, group, false); 
+        return modifyGroup(existing, group, false);
     }
-    
+
     private Group modifyGroup(final Group existing, final Group group, boolean withActivate)
         throws UserNotFoundException, TransientException,
                AccessControlException, GroupNotFoundException
@@ -575,7 +525,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new UnsupportedOperationException(
                     "Support for groups properties not available");
         }
-        
+
         boolean adminChanges = false;
 
         List<Modification> mods = new ArrayList<Modification>();
@@ -595,90 +545,89 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         {
             mods.add(new Modification(ModificationType.REPLACE, "description", group.description));
         }
-        
-        Set<String> newMembers = new HashSet<String>();
-        for (User<?> member : group.getUserMembers())
-        {
-            DN memberDN = userPersist.getUserDN(member);
-            newMembers.add(memberDN.toNormalizedString());
-        }
-        for (Group gr : group.getGroupMembers())
+        try
         {
-            if (!checkGroupExists(gr.getID()))
+            Set<String> newMembers = new HashSet<String>();
+            for (User<?> member : group.getUserMembers())
             {
-                throw new GroupNotFoundException(gr.getID());
+                DN memberDN = userPersist.getUserDN(member);
+                newMembers.add(memberDN.toNormalizedString());
             }
-            DN grDN = getGroupDN(gr.getID());
-            newMembers.add(grDN.toNormalizedString());
-        }
-        
-        Set<String> newAdmins = new HashSet<String>();
-        Set<User<? extends Principal>> existingUserAdmins = new HashSet<User<? extends Principal>>(0);
-        if (existing != null)
-        {
-        	existingUserAdmins = existing.getUserAdmins();
-        }
-        for (User<?> member : group.getUserAdmins())
-        {
-        	DN memberDN = userPersist.getUserDN(member);
-        	newAdmins.add(memberDN.toNormalizedString());
-        	if (!existingUserAdmins.contains(member))
+            for (Group gr : group.getGroupMembers())
             {
-            	adminChanges = true;
+                if (!checkGroupExists(gr.getID()))
+                {
+                    throw new GroupNotFoundException(gr.getID());
+                }
+                DN grDN = getGroupDN(gr.getID());
+                newMembers.add(grDN.toNormalizedString());
             }
-        }
-        
-        Set<Group> existingGroupAdmins = new HashSet<Group>(0);
-        if (existing != null)
-        {
-        	existingGroupAdmins = existing.getGroupAdmins();
-        }
-        for (Group gr : group.getGroupAdmins())
-        {
-            if (!checkGroupExists(gr.getID()))
+
+            Set<String> newAdmins = new HashSet<String>();
+            Set<User<? extends Principal>> existingUserAdmins = new HashSet<User<? extends Principal>>(0);
+            if (existing != null)
             {
-                throw new GroupNotFoundException(gr.getID());
+                    existingUserAdmins = existing.getUserAdmins();
+            }
+            for (User<?> member : group.getUserAdmins())
+            {
+                    DN memberDN = userPersist.getUserDN(member);
+                    newAdmins.add(memberDN.toNormalizedString());
+                    if (!existingUserAdmins.contains(member))
+                {
+                    adminChanges = true;
+                }
             }
 
-        	DN grDN = getGroupDN(gr.getID());
-        	newAdmins.add(grDN.toNormalizedString());
-        	if (!existingGroupAdmins.contains(gr))
+            Set<Group> existingGroupAdmins = new HashSet<Group>(0);
+            if (existing != null)
             {
-            	adminChanges = true;
+                    existingGroupAdmins = existing.getGroupAdmins();
+            }
+            for (Group gr : group.getGroupAdmins())
+            {
+                if (!checkGroupExists(gr.getID()))
+                {
+                    throw new GroupNotFoundException(gr.getID());
+                }
+
+                    DN grDN = getGroupDN(gr.getID());
+                    newAdmins.add(grDN.toNormalizedString());
+                    if (!existingGroupAdmins.contains(gr))
+                {
+                    adminChanges = true;
+                }
+            }
+
+            mods.add(new Modification(ModificationType.REPLACE, "uniquemember",
+                    (String[]) newMembers.toArray(new String[newMembers.size()])));
+            adminMods.add(new Modification(ModificationType.REPLACE, "uniquemember",
+                    (String[]) newAdmins.toArray(new String[newAdmins.size()])));
+
+            // modify admin group first (if necessary)
+            if (adminChanges)
+            {
+            ModifyRequest modifyRequest = new ModifyRequest(getAdminGroupDN(group.getID()), adminMods);
+
+                modifyRequest.addControl(
+                        new ProxiedAuthorizationV2RequestControl(
+                                "dn:" + getSubjectDN().toNormalizedString()));
+
+                LdapDAO.checkLdapResult(getConnection().modify(modifyRequest).getResultCode());
             }
-        }
 
-        mods.add(new Modification(ModificationType.REPLACE, "uniquemember", 
-                (String[]) newMembers.toArray(new String[newMembers.size()])));
-        adminMods.add(new Modification(ModificationType.REPLACE, "uniquemember", 
-                (String[]) newAdmins.toArray(new String[newAdmins.size()])));
-        
-        try
-        {
-        	// modify admin group first (if necessary)
-        	if (adminChanges)
-        	{   
-                ModifyRequest modifyRequest = new ModifyRequest(getAdminGroupDN(group.getID()), adminMods);
-                
-	            modifyRequest.addControl(
-	                    new ProxiedAuthorizationV2RequestControl(
-	                            "dn:" + getSubjectDN().toNormalizedString()));
-	            LdapDAO.checkLdapResult(getConnection().
-	                    modify(modifyRequest).getResultCode());
-        	}
-            
             // modify the group itself now
-        	ModifyRequest modifyRequest = new ModifyRequest(getGroupDN(group.getID()), mods);
+            ModifyRequest modifyRequest = new ModifyRequest(getGroupDN(group.getID()), mods);
 
             modifyRequest.addControl(
                     new ProxiedAuthorizationV2RequestControl(
                             "dn:" + getSubjectDN().toNormalizedString()));
-            LdapDAO.checkLdapResult(getConnection().
-                    modify(modifyRequest).getResultCode());
+
+            LdapDAO.checkLdapResult(getConnection().modify(modifyRequest).getResultCode());
         }
         catch (LDAPException e1)
         {
-        	logger.debug("Modify Exception: " + e1, e1);
+            logger.debug("Modify Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
         try
@@ -694,16 +643,15 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (GroupNotFoundException e)
         {
-            throw new RuntimeException(
-                    "BUG: modified group not found (" + group.getID() + ")");
+            throw new RuntimeException("BUG: modified group not found (" + group.getID() + ")");
         }
     }
 
     /**
      * Deletes the group.
-     * 
+     *
      * @param groupID The group to delete
-     * 
+     *
      * @throws GroupNotFoundException If the group was not found.
      * @throws TransientException If an temporary, unexpected problem occurred.
      */
@@ -714,19 +662,19 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         deleteGroup(getGroupDN(groupID), groupID, false);
         deleteGroup(getAdminGroupDN(groupID), groupID, true);
     }
-    
-    private void deleteGroup(final DN groupDN, final String groupID, 
+
+    private void deleteGroup(final DN groupDN, final String groupID,
                              final boolean isAdmin)
         throws GroupNotFoundException, TransientException,
                AccessControlException
     {
-        Group group = getGroup(groupDN, groupID, true);
+        Group group = getGroup(groupDN, groupID, GROUP_AND_MEMBER_ATTRS);
         List<Modification> modifs = new ArrayList<Modification>();
         modifs.add(new Modification(ModificationType.ADD, "nsaccountlock", "true"));
-        
+
         if (isAdmin)
         {
-            if (!group.getGroupAdmins().isEmpty() || 
+            if (!group.getGroupAdmins().isEmpty() ||
                 !group.getUserAdmins().isEmpty())
             {
                 modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
@@ -734,7 +682,7 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         else
         {
-            if (!group.getGroupMembers().isEmpty() || 
+            if (!group.getGroupMembers().isEmpty() ||
                 !group.getUserMembers().isEmpty())
             {
                 modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
@@ -752,35 +700,32 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e1)
         {
-        	logger.debug("Delete Exception: " + e1, e1);
+            logger.debug("Delete Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
-        
+
         try
         {
-            getGroup(group.getID());
-            throw new RuntimeException("BUG: group not deleted " + 
-                                       group.getID());
+            getGroup(getGroupDN(group.getID()), null, GROUP_ATTRS);
+            throw new RuntimeException("BUG: group not deleted " + group.getID());
         }
-        catch (GroupNotFoundException ignore) {}
+        catch (GroupNotFoundException ignore) { }
     }
-    
+
     /**
-     * Obtain a Collection of Groups that fit the given query.
-     * 
+     * Obtain a Collection of Groups that fit the given query. The returned groups
+     * will not include members.
+     *
      * @param userID The userID.
      * @param role Role of the user, either owner, member, or read/write.
      * @param groupID The Group ID.
-     * 
-     * @return Collection of Groups
-     *         matching GROUP_READ_ACI.replace(ACTUAL_GROUP_TOKEN,
-     *         readGrDN.toNormalizedString()) the query, or empty
-     *         Collection. Never null.
+     *
+     * @return possibly empty collection of Group that match the query
      * @throws TransientException  If an temporary, unexpected problem occurred.
      * @throws UserNotFoundException
      * @throws GroupNotFoundException
      */
-    public Collection<Group> getGroups(final T userID, final Role role, 
+    public Collection<Group> getGroups(final T userID, final Role role,
                                        final String groupID)
         throws TransientException, AccessControlException,
                GroupNotFoundException, UserNotFoundException
@@ -796,107 +741,162 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             // no anonymous searches
             throw new AccessControlException("Not authorized to search");
         }
-        
-        Collection<DN> groupDNs = new HashSet<DN>();
+
+        Collection<Group> ret;
         if (role == Role.OWNER)
         {
-            groupDNs.addAll(getOwnerGroups(user, userDN, groupID));
-        }
-        else if (role == Role.MEMBER)
-        {
-            groupDNs.addAll(getMemberGroups(user, userDN, groupID, false));
-        }
-        else if (role == Role.ADMIN)
-        {
-            groupDNs.addAll(getMemberGroups(user, userDN, groupID, true));
+            ret = getOwnerGroups(user, userDN, groupID);
         }
-        
-        if (logger.isDebugEnabled())
+        else
         {
-            for (DN dn : groupDNs)
+            Collection<DN> groupDNs = null;
+
+            if (role == Role.MEMBER)
             {
-                logger.debug("Search adding DN: " + dn);
+                groupDNs = getMemberGroups(user, userDN, groupID, false);
             }
-        }
-        
-        Collection<Group> groups = new HashSet<Group>();
-        try
-        {
-            for (DN groupDN : groupDNs)
+            else if (role == Role.ADMIN)
             {
-                if (role == Role.ADMIN)
-                {
-                    groupDN = new DN(groupDN.getRDNString() + "," + config.getGroupsDN());
-                }
-                try
-                {
-                    groups.add(getGroup(groupDN));
-                    logger.debug("Search adding group: " + groupDN);
-                }
-                catch (GroupNotFoundException e)
+                groupDNs = getMemberGroups(user, userDN, groupID, true);
+            }
+            else
+                throw new IllegalArgumentException("null role");
+
+            ret = new ArrayList<Group>();
+            try
+            {
+                for (DN groupDN : groupDNs)
                 {
-                    final String message = "BUG: group " + groupDN + " not found but " +
-                                           "membership exists (" + userID + ")";
-                    logger.error(message);
-                    //throw new IllegalStateException(message);
+                    if (role == Role.ADMIN)
+                    {
+                        groupDN = new DN(groupDN.getRDNString() + "," + config.getGroupsDN());
+                    }
+                    try
+                    {
+                        Group g = createGroupFromDN(groupDN);
+                        if (isDetailedSearch(g, role))
+                        {
+                            g = getGroup(groupDN, null, GROUP_ATTRS);
+                        }
+                            logger.debug("found group: " + g.getID());
+                            ret.add(g);
+                    }
+                    catch (GroupNotFoundException e)
+                    {
+                        final String message = "BUG: group " + groupDN + " not found but " +
+                                               "membership exists (" + userID + ")";
+                        logger.error(message);
+                    }
                 }
             }
+            catch (LDAPException e)
+            {
+                logger.debug("getGroups Exception: " + e, e);
+                throw new TransientException("Error getting group", e);
+            }
         }
-        catch (LDAPException e)
+
+        logger.debug("found: " + ret.size() + "groups matching " + userID + "," + role + "," + groupID);
+        return ret;
+    }
+
+    // some pretty horrible hacks to avoid querying LDAP for group details...
+    private Group createGroupFromDN(DN groupDN)
+    {
+        String cn = groupDN.getRDNString();
+        String[] parts = cn.split("=");
+        if (parts.length == 2 && parts[0].equals("cn"))
         {
-        	logger.debug("getGroups Exception: " + e, e);
-            throw new TransientException("Error getting group", e);
+            return new Group(parts[1]);
         }
-        return groups;
+        throw new RuntimeException("BUG: failed to extract group name from " + groupDN.toString());
     }
-    
-    protected Collection<DN> getOwnerGroups(final User<T> user, 
+    // this gets filled by the LdapgroupPersistence
+    GroupDetailSelector searchDetailSelector;
+
+    private boolean isDetailedSearch(Group g, Role r)
+    {
+        if (searchDetailSelector == null)
+            return true;
+        return searchDetailSelector.isDetailedSearch(g, r);
+    }
+    // end of horribleness
+
+    protected Collection<Group> getOwnerGroups(final User<T> user,
                                             final DN userDN,
                                             final String groupID)
-        throws TransientException, AccessControlException,
-               GroupNotFoundException, UserNotFoundException
+        throws TransientException, AccessControlException
     {
-        Collection<DN> groupDNs = new HashSet<DN>();
+        Collection<Group> ret = new ArrayList<Group>();
         try
-        {                           
-            Filter filter = Filter.createEqualityFilter("owner", 
-                                                        userDN.toString());
+        {
+            Filter filter = Filter.createNOTFilter(Filter.createPresenceFilter("nsaccountlock"));
+
+            filter = Filter.createANDFilter(filter,
+                Filter.createEqualityFilter("owner", userDN.toNormalizedString()));
+
             if (groupID != null)
             {
-                getGroup(groupID);
-                filter = Filter.createANDFilter(filter, 
-                                Filter.createEqualityFilter("cn", groupID));
+                DN groupDN = getGroupDN(groupID);
+                filter = Filter.createANDFilter(filter,
+                    Filter.createEqualityFilter("entrydn", groupDN.toNormalizedString()));
             }
-            
+
             SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, "entrydn", "nsaccountlock");
-            
+                    config.getGroupsDN(), SearchScope.SUB, filter, GROUP_ATTRS);
+
             searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
                             getSubjectDN().toNormalizedString()));
-            
+
             SearchResult results = getConnection().search(searchRequest);
             for (SearchResultEntry result : results.getSearchEntries())
             {
-                String entryDN = result.getAttributeValue("entrydn");
-                // make sure the group isn't deleted
-                if (result.getAttribute("nsaccountlock") == null)
-                {
-                    groupDNs.add(new DN(entryDN));
-                }
-                
+                ret.add(createGroupFromEntry(result, GROUP_ATTRS));
             }
         }
         catch (LDAPException e1)
         {
-        	logger.debug("getOwnerGroups Exception: " + e1, e1);
+            logger.debug("getOwnerGroups Exception: " + e1, e1);
             LdapDAO.checkLdapResult(e1.getResultCode());
         }
-        return groupDNs; 
+        return ret;
+    }
+
+    private Group createGroupFromEntry(SearchResultEntry result, String[] attributes)
+        throws LDAPException
+    {
+        if (result.getAttribute("nsaccountlock") != null)
+        {
+            throw new RuntimeException("BUG: found group with nsaccountlock set: " + result.getAttributeValue("entrydn").toString());
+        }
+
+        String entryDN = result.getAttributeValue("entrydn");
+        String groupName = result.getAttributeValue("cn");
+        if (attributes == PUB_GROUP_ATTRS)
+            return new Group(groupName);
+
+        DN ownerDN = result.getAttributeValueAsDN("owner");
+        if (ownerDN == null)
+            throw new AccessControlException(groupName);
+        try
+        {
+            User owner = userPersist.getX500User(ownerDN);
+            Group g = new Group(groupName, owner);
+            if (result.hasAttribute("description"))
+                g.description = result.getAttributeValue("description");
+            if (result.hasAttribute("modifytimestamp"))
+                g.lastModified = result.getAttributeValueAsDate("modifytimestamp");
+            return g;
+        }
+        catch(UserNotFoundException ex)
+        {
+            throw new RuntimeException("Invalid state: owner does not exist: " + ownerDN + " group: " + entryDN);
+        }
     }
-    
-    protected Collection<DN> getMemberGroups(final User<T> user, 
-                                             final DN userDN, 
+
+    protected Collection<DN> getMemberGroups(final User<T> user,
+                                             final DN userDN,
                                              final String groupID,
                                              final boolean isAdmin)
         throws TransientException, AccessControlException,
@@ -922,114 +922,17 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         else
         {
-            Collection<DN> memberGroupDNs = 
+            Collection<DN> memberGroupDNs =
                     userPersist.getUserGroups(user.getUserID(), isAdmin);
             groupDNs.addAll(memberGroupDNs);
         }
         return groupDNs;
     }
-    
-    /**
-     * Returns a group based on its LDAP DN. The returned group does not contain
-     * members or admins
-     * 
-     * @param groupDN
-     * @return
-     * @throws com.unboundid.ldap.sdk.LDAPException
-     * @throws ca.nrc.cadc.ac.GroupNotFoundException - if group does not exist,
-     * it's deleted or caller has no access to it.
-     */
-    protected Group getGroup(final DN groupDN)
-        throws LDAPException, GroupNotFoundException, UserNotFoundException
-    {
-        Filter filter = Filter.createEqualityFilter("entrydn", 
-                                                    groupDN.toNormalizedString());
-        
-        SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, 
-                    "cn", "description", "owner", "nsaccountlock");
-            
-        searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
-                            getSubjectDN().toNormalizedString()));
-            
-        SearchResultEntry searchResult = 
-                getConnection().searchForEntry(searchRequest);
 
-        if (searchResult == null)
-        {
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-        
-        if (searchResult.getAttribute("nsaccountlock") != null)
-        {
-            // deleted group
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-
-        Group group = new Group(searchResult.getAttributeValue("cn"),
-                                userPersist.getX500User(
-                                    new DN(searchResult.getAttributeValue(
-                                        "owner"))));
-        group.description = searchResult.getAttributeValue("description");
-        return group;
-    }
-
-    /**
-     * Returns a group ID corresponding to a DN. Although the groupID can be
-     * deduced from the group DN, this method checks if the group exists and
-     * it's active and throws an exception if any of those conditions are not
-     * met.
-     * 
-     * @param groupDN
-     * @return
-     * @throws com.unboundid.ldap.sdk.LDAPException
-     * @throws ca.nrc.cadc.ac.GroupNotFoundException - Group not found or not
-     * active
-     */
-    protected String getGroupID(final DN groupDN)
-        throws LDAPException, GroupNotFoundException
-    {
-        Filter filter = Filter.createEqualityFilter("entrydn", 
-                                                    groupDN.toNormalizedString());
-        
-        SearchRequest searchRequest =  new SearchRequest(
-                    config.getGroupsDN(), SearchScope.SUB, filter, 
-                    "cn", "nsaccountlock");
-            
-        searchRequest.addControl(
-                    new ProxiedAuthorizationV2RequestControl("dn:" + 
-                            getSubjectDN().toNormalizedString()));
-            
-        SearchResultEntry searchResult = 
-                getConnection().searchForEntry(searchRequest);
-
-        if (searchResult == null)
-        {
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-        
-        if (searchResult.getAttribute("nsaccountlock") != null)
-        {
-            // deleted group
-            String msg = "Group not found " + groupDN;
-            logger.debug(msg);
-            throw new GroupNotFoundException(groupDN.toNormalizedString());
-        }
-
-        return searchResult.getAttributeValue("cn");
-    }
-    
     /**
-     * 
+     *
      * @param groupID
-     * @return 
+     * @return
      */
     protected DN getGroupDN(final String groupID) throws TransientException
     {
@@ -1039,16 +942,16 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e)
         {
-        	logger.debug("getGroupDN Exception: " + e, e);
+            logger.debug("getGroupDN Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
         }
         throw new IllegalArgumentException(groupID + " not a valid group ID");
     }
-    
+
     /**
-     * 
+     *
      * @param groupID
-     * @return 
+     * @return
      */
     protected DN getAdminGroupDN(final String groupID) throws TransientException
     {
@@ -1058,24 +961,24 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
         }
         catch (LDAPException e)
         {
-        	logger.debug("getAdminGroupDN Exception: " + e, e);
+            logger.debug("getAdminGroupDN Exception: " + e, e);
             LdapDAO.checkLdapResult(e.getResultCode());
         }
         throw new IllegalArgumentException(groupID + " not a valid group ID");
     }
-    
+
     /**
-     * 
+     *
      * @param owner
      * @return
-     * @throws UserNotFoundException 
+     * @throws UserNotFoundException
      */
     protected boolean isCreatorOwner(final User<? extends Principal> owner)
         throws UserNotFoundException
     {
         try
         {
-            User<X500Principal> subjectUser = 
+            User<X500Principal> subjectUser =
                     userPersist.getX500User(getSubjectDN());
             if (subjectUser.equals(owner))
             {
@@ -1089,18 +992,20 @@ public class LdapGroupDAO<T extends Principal> extends LdapDAO
             throw new RuntimeException(e);
         }
     }
-    
-    private boolean checkGroupExists(String groupID) 
-            throws TransientException
+
+    private boolean checkGroupExists(String groupID)
+            throws LDAPException, TransientException
     {
-        for (String groupName : getGroupNames())
+        try
         {
-            if (groupName.equalsIgnoreCase(groupID))
-            {
-                return true;
-            }
+            Group g = getGroup(getGroupDN(groupID), groupID, PUB_GROUP_ATTRS);
+            return true;
+        }
+        catch(GroupNotFoundException ex)
+        {
+            return false;
         }
-        return false;
+        finally { }
     }
 
 }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
index e66dc2e5960f289be4050c3d87429b0a3f93c38d..9f66002c0eb4cd86c8e328440456b3f54408a585 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
@@ -79,6 +79,7 @@ import ca.nrc.cadc.ac.GroupAlreadyExistsException;
 import ca.nrc.cadc.ac.GroupNotFoundException;
 import ca.nrc.cadc.ac.Role;
 import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupDetailSelector;
 import ca.nrc.cadc.ac.server.GroupPersistence;
 import ca.nrc.cadc.net.TransientException;
 
@@ -88,12 +89,19 @@ public class LdapGroupPersistence<T extends Principal>
     private static final Logger log = 
             Logger.getLogger(LdapGroupPersistence.class);
     private final LdapConfig config;
+    
+    private GroupDetailSelector detailSelector;
 
     public LdapGroupPersistence()
     {
         config = LdapConfig.getLdapConfig();
     }
     
+    protected void setDetailSelector(GroupDetailSelector gds)
+    {
+        this.detailSelector = gds;
+    }
+    
     public Collection<String> getGroupNames()
         throws TransientException, AccessControlException
     {
@@ -233,6 +241,7 @@ public class LdapGroupPersistence<T extends Principal>
         {
             userDAO = new LdapUserDAO<T>(config);
             groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            groupDAO.searchDetailSelector = detailSelector;
             Collection<Group> ret = groupDAO.getGroups(userID, role, groupID);
             return ret;
         }
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
index f576ec6ba47de6cf4cff2ee3e8fb394d142e3a32..80661d4573cc2e4efaac40fcbdc5e609de09053d 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/ACSearchRunner.java
@@ -335,6 +335,23 @@ public class ACSearchRunner implements JobRunner
 //            }
         }
         */
+        catch(IllegalArgumentException ex)
+        {
+            logInfo.setSuccess(true);
+            logInfo.setMessage(ex.getMessage());
+            log.debug("FAIL", ex);
+            
+            syncOut.setResponseCode(400);
+            syncOut.setHeader("Content-Type", "text/plain");
+            try
+            {
+                syncOut.getOutputStream().write(ex.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+        }
         catch (AccessControlException t)
         {
             logInfo.setSuccess(true);
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
index 096b5827a25ad73b5efcb91427879d0c3cd28943..fb9a2dbfff49f2851412f52663898931a3d5b695 100755
--- a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/groups/GroupServlet.java
@@ -80,7 +80,6 @@ import org.apache.log4j.Logger;
 
 import ca.nrc.cadc.ac.server.web.SyncOutput;
 import ca.nrc.cadc.auth.AuthenticationUtil;
-import ca.nrc.cadc.util.StringUtil;
 
 /**
  * Servlet for handling all requests to /groups
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 b5f7b7fc706285aec7404148022f0a4ffae7f2a1..24b055459faa0ebe77d4fb669f3431118b147869 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
@@ -494,11 +494,11 @@ public class LdapGroupDAOTest extends AbstractLdapDAOTest
                     for (Group group : groups)
                     {
                         log.debug("admin group: " + group.getID());
-                        if (group.getID().equals(testGroup1ID))
+                        if (group.getID().equalsIgnoreCase(testGroup1ID))
                         {
                             found1 = true;
                         }
-                        if (group.getID().equals(testGroup2ID))
+                        if (group.getID().equalsIgnoreCase(testGroup2ID))
                         {
                             found2 = true;
                         }
@@ -861,18 +861,10 @@ public class LdapGroupDAOTest extends AbstractLdapDAOTest
                     getGroupDAO().getGroups(unknownPrincipal, Role.OWNER, 
                                                groupID);
                     fail("searchGroups with unknown user should throw " + 
-                         "UserNotFoundException");
+                         "AccessControlException");
                 }
                 catch (AccessControlException ignore) {}
                 
-                try
-                {
-                    getGroupDAO().getGroups(daoTestPrincipal1, Role.OWNER, 
-                                               "foo");
-                    fail("searchGroups with unknown user should throw " + 
-                         "GroupNotFoundException");
-                }
-                catch (GroupNotFoundException ignore) {}
                 return null;
             }
         });
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
index 6a91450bc9acc1e27da28ae2f227edfa2fe8fd16..8dac1009e59bf92e492678dfb3837e7088aa0658 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
@@ -155,6 +155,11 @@ public class Group
         return owner;
     }
 
+    public void setOwner(User<? extends Principal> owner)
+    {
+        this.owner = owner;
+    }
+
     /**
      * 
      * @return a set of properties associated with a group
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 bad17d1f5f80cee819082265ca7f344940ac14b1..759e413e165e11ca1698eba743e2e5e6c2737212 100755
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -68,28 +68,6 @@
  */
 package ca.nrc.cadc.ac.client;
 
-import ca.nrc.cadc.ac.Group;
-import ca.nrc.cadc.ac.GroupAlreadyExistsException;
-import ca.nrc.cadc.ac.GroupNotFoundException;
-import ca.nrc.cadc.ac.Role;
-import ca.nrc.cadc.ac.User;
-import ca.nrc.cadc.ac.UserNotFoundException;
-import ca.nrc.cadc.ac.xml.GroupListReader;
-import ca.nrc.cadc.ac.xml.GroupReader;
-import ca.nrc.cadc.ac.xml.GroupWriter;
-import ca.nrc.cadc.auth.AuthenticationUtil;
-import ca.nrc.cadc.auth.HttpPrincipal;
-import ca.nrc.cadc.auth.SSLUtil;
-import ca.nrc.cadc.net.HttpDownload;
-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.apache.log4j.Logger;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLSocketFactory;
-import javax.security.auth.Subject;
 import java.io.BufferedReader;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -111,12 +89,36 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+
+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.Role;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.xml.GroupListReader;
+import ca.nrc.cadc.ac.xml.GroupReader;
+import ca.nrc.cadc.ac.xml.GroupWriter;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.SSLUtil;
+import ca.nrc.cadc.net.HttpDownload;
+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 ca.nrc.cadc.net.event.TransferEvent;
+import ca.nrc.cadc.net.event.TransferListener;
+
 
 /**
  * Client class for performing group searching and group actions
  * with the access control web service.
  */
-public class GMSClient
+public class GMSClient implements TransferListener
 {
     private static final Logger log = Logger.getLogger(GMSClient.class);
 
@@ -130,10 +132,10 @@ public class GMSClient
      * Constructor.
      *
      * @param baseURL The URL of the supporting access control web service
-     *                obtained from the registry.
+     * obtained from the registry.
      */
-    public GMSClient(final String baseURL)
-            throws IllegalArgumentException
+    public GMSClient(String baseURL)
+        throws IllegalArgumentException
     {
         if (baseURL == null)
         {
@@ -159,71 +161,26 @@ public class GMSClient
         }
     }
 
-    /**
-     * Get a list of groups.
-     *
-     * @return The list of groups.
-     */
-    public List<Group> getGroups()
+    public void transferEvent(TransferEvent te)
     {
-        throw new UnsupportedOperationException("Not yet implemented");
+        if ( TransferEvent.RETRYING == te.getState() )
+            log.debug("retry after request failed, reason: "  + te.getError());
     }
 
+    public String getEventHeader()
+    {
+        return null; // no custom eventID header
+    }
 
 
     /**
-     * Obtain all of the users as userID - name in JSON format.
+     * Get a list of groups.
      *
-     * @return List of HTTP Principal users.
-     * @throws IOException Any errors in reading.
+     * @return The list of groups.
      */
-    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
+    public List<Group> getGroups()
     {
-        final URL usersListURL = new URL(this.baseURL + "/users");
-        return new HttpDownload(usersListURL,
-                                new JsonUserListInputStreamWrapper(webUsers));
+        throw new UnsupportedOperationException("Not yet implemented");
     }
 
     /**
@@ -233,13 +190,13 @@ 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());
@@ -310,7 +267,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());
@@ -323,8 +280,7 @@ 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) ||
@@ -365,7 +321,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());
@@ -373,28 +329,26 @@ 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
                 {
-                    @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);
-                        }
+                    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);
+                }
+            }
+        });
 
         httpDownload.setSSLSocketFactory(getSSLSocketFactory());
         httpDownload.run();
@@ -410,7 +364,7 @@ public class GMSClient
                       errMessage);
 
             if ((responseCode == 401) || (responseCode == 403) ||
-                (responseCode == -1))
+                    (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -433,15 +387,14 @@ 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());
@@ -457,8 +410,10 @@ public class GMSClient
         HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(),
                                          "application/xml", true);
         transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.setTransferListener(this);
         transfer.run();
 
+
         Throwable error = transfer.getThrowable();
         if (error != null)
         {
@@ -475,15 +430,10 @@ 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);
         }
@@ -511,7 +461,7 @@ 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());
@@ -536,7 +486,7 @@ public class GMSClient
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception e)
+        catch(Exception e)
         {
             throw new AccessControlException(e.getMessage());
         }
@@ -548,7 +498,7 @@ public class GMSClient
                       errMessage);
 
             if ((responseCode == 401) || (responseCode == 403) ||
-                (responseCode == -1))
+                    (responseCode == -1))
             {
                 throw new AccessControlException(errMessage);
             }
@@ -570,13 +520,13 @@ 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/" +
@@ -620,15 +570,14 @@ 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");
@@ -665,15 +614,10 @@ 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);
         }
@@ -690,7 +634,7 @@ public class GMSClient
      */
     public void removeGroupMember(String targetGroupName,
                                   String groupMemberName)
-            throws GroupNotFoundException, AccessControlException, IOException
+        throws GroupNotFoundException, AccessControlException, IOException
     {
         URL removeGroupMemberURL = new URL(this.baseURL + "/groups/" +
                                            targetGroupName + "/groupMembers/" +
@@ -712,15 +656,13 @@ public class GMSClient
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
 
-        // Try to handle anonymous access and throw AccessControlException 
+        // Try to handle anonymous access and throw AccessControlException
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore)
-        {
-        }
+        catch (Exception ignore) {}
 
         if (responseCode != 200)
         {
@@ -750,15 +692,14 @@ 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");
@@ -784,15 +725,13 @@ public class GMSClient
                     .setSSLSocketFactory(getSSLSocketFactory());
         }
 
-        // Try to handle anonymous access and throw AccessControlException 
+        // Try to handle anonymous access and throw AccessControlException
         int responseCode = -1;
         try
         {
             responseCode = conn.getResponseCode();
         }
-        catch (Exception ignore)
-        {
-        }
+        catch (Exception ignore) {}
 
         if (responseCode != 200)
         {
@@ -812,15 +751,10 @@ 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);
         }
@@ -830,22 +764,22 @@ 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);
+        List<Group> cachedGroups = getCachedGroups(userID, role, true);
         if (cachedGroups != null)
         {
             return cachedGroups;
@@ -914,19 +848,19 @@ public class GMSClient
      * 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);
     }
@@ -936,35 +870,27 @@ public class GMSClient
      * 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)
+        Group cachedGroup = getCachedGroup(userID, groupName, role);
+        if (cachedGroup != null)
         {
-            int index = cachedGroups.indexOf(new Group(groupName));
-            if (index != -1)
-            {
-                return cachedGroups.get(index);
-            }
-            else
-            {
-                return null;
-            }
+            return cachedGroup;
         }
 
         String idType = AuthenticationUtil.getPrincipalType(userID);
@@ -1024,9 +950,9 @@ public class GMSClient
             }
             if (groups.size() == 1)
             {
-                // don't cache these results as it is not a complete
-                // list of memberships--it only applies to one group.
-                return groups.get(0);
+                Group ret = groups.get(0);
+                addCachedGroup(userID, ret, role);
+                return ret;
             }
             throw new IllegalStateException(
                     "Duplicate membership for " + id + " in group " + groupName);
@@ -1040,19 +966,19 @@ public class GMSClient
 
     /**
      * 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);
     }
@@ -1060,17 +986,17 @@ public class GMSClient
     /**
      * 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;
@@ -1082,16 +1008,13 @@ 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();
@@ -1114,12 +1037,9 @@ 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");
-            }
+                        + this.getClass().getSimpleName()
+                        + ": subject change not supported for internal SSLSocketFactory");
         }
         return this.mySocketFactory;
     }
@@ -1128,15 +1048,13 @@ public class GMSClient
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
-
         if (subject != null)
         {
-            log.debug("Clearing cache");
-            subject.getPrivateCredentials().clear();
+            subject.getPrivateCredentials().remove(new GroupMemberships());
         }
     }
 
-    protected List<Group> getCachedGroups(Principal userID, Role role)
+    protected GroupMemberships getGroupCache(Principal userID)
     {
         AccessControlContext acContext = AccessController.getContext();
         Subject subject = Subject.getSubject(acContext);
@@ -1144,46 +1062,81 @@ public class GMSClient
         // only consult cache if the userID is of the calling subject
         if (userIsSubject(userID, subject))
         {
-            Set groupCredentialSet = subject
-                    .getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) &&
-                (groupCredentialSet.size() == 1))
+            Set<GroupMemberships> gset = subject.getPrivateCredentials(GroupMemberships.class);
+            if (gset == null || gset.isEmpty())
             {
-                Iterator i = groupCredentialSet.iterator();
-                GroupMemberships groupMemberships = ((GroupMemberships) i
-                        .next());
-                return groupMemberships.memberships.get(role);
+                GroupMemberships mems = new GroupMemberships();
+                subject.getPrivateCredentials().add(mems);
+                return mems;
             }
+            GroupMemberships mems = gset.iterator().next();
+            return mems;
+        }
+        return null; // no cache
+    }
+
+    protected Group getCachedGroup(Principal userID, String groupID, Role role)
+    {
+        List<Group> groups = getCachedGroups(userID, role, false);
+        if (groups == null)
+            return null; // no cache
+        for (Group g : groups)
+        {
+            if (g.getID().equals(groupID))
+                return g;
         }
         return null;
     }
+    protected List<Group> getCachedGroups(Principal userID, Role role, boolean complete)
+    {
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return null; // no cache
 
-    protected void setCachedGroups(Principal userID, List<Group> groups, Role role)
+        Boolean cacheState = mems.complete.get(role);
+        if (!complete || Boolean.TRUE.equals(cacheState))
+            return mems.memberships.get(role);
+
+        // caller wanted complete and we don't have that
+        return null;
+    }
+
+    protected void addCachedGroup(Principal userID, Group group, Role role)
     {
-        AccessControlContext acContext = AccessController.getContext();
-        Subject subject = Subject.getSubject(acContext);
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return; // no cache
 
-        // only save to cache if the userID is of the calling subject
-        if (userIsSubject(userID, subject))
+        List<Group> groups = mems.memberships.get(role);
+        if (groups == null)
         {
-            log.debug("Caching groups for " + userID + ", role " + role);
+            groups = new ArrayList<Group>();
+            mems.complete.put(role, Boolean.FALSE);
+            mems.memberships.put(role, groups);
+        }
+        if (!groups.contains(group))
+            groups.add(group);
+    }
 
-            final GroupMemberships groupCredentials;
-            Set groupCredentialSet = subject
-                    .getPrivateCredentials(GroupMemberships.class);
-            if ((groupCredentialSet != null) &&
-                (groupCredentialSet.size() == 1))
-            {
-                Iterator i = groupCredentialSet.iterator();
-                groupCredentials = ((GroupMemberships) i.next());
-            }
-            else
-            {
-                groupCredentials = new GroupMemberships();
-                subject.getPrivateCredentials().add(groupCredentials);
-            }
+    protected void setCachedGroups(Principal userID, List<Group> groups, Role role)
+    {
+        GroupMemberships mems = getGroupCache(userID);
+        if (mems == null)
+            return; // no cache
 
-            groupCredentials.memberships.put(role, groups);
+        log.debug("Caching groups for " + userID + ", role " + role);
+        List<Group> cur = mems.memberships.get(role);
+        if (cur == null)
+        {
+            cur = new ArrayList<Group>();
+            mems.complete.put(role, Boolean.FALSE);
+            mems.memberships.put(role, cur);
+        }
+        for (Group group : groups)
+        {
+            if (!cur.contains(group))
+                cur.add(group);
+            mems.complete.put(role, Boolean.TRUE);
         }
     }
 
@@ -1196,7 +1149,7 @@ public class GMSClient
 
         for (Principal subjectPrincipal : subject.getPrincipals())
         {
-            if (subjectPrincipal.equals(userID))
+            if (AuthenticationUtil.equals(subjectPrincipal, userID))
             {
                 return true;
             }
@@ -1205,17 +1158,31 @@ public class GMSClient
     }
 
     /**
-     * Class used to hold list of groups in which
-     * a user is a member.
+     * Class used to hold list of groups in which a user is known to be a member.
      */
-    protected class GroupMemberships
+    protected class GroupMemberships implements Comparable
     {
         Map<Role, List<Group>> memberships = new HashMap<Role, List<Group>>();
+        Map<Role, Boolean> complete = new HashMap<Role, Boolean>();
 
         protected GroupMemberships()
         {
         }
 
+        // only allow one in a set - makes clearCache simple too
+        public boolean equals(Object rhs)
+        {
+            if (rhs != null && rhs instanceof GroupMemberships)
+                return true;
+            return false;
+        }
+
+        public int compareTo(Object t)
+        {
+            if (this.equals(t))
+                return 0;
+            return -1; // wonder if this is sketchy
+        }
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4a1fa6d6561729b0e67bfcffe8b46d894c9d4a2
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClientMain.java
@@ -0,0 +1,276 @@
+/*
+ ************************************************************************
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+ *
+ *  (c) 2014.                            (c) 2014.
+ *  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/>.
+ *
+ *  $Revision: 4 $
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.client;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.User;
+import java.net.URI;
+import java.net.URL;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.auth.CertCmdArgUtil;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.reg.client.RegistryClient;
+import ca.nrc.cadc.util.ArgumentMap;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.util.Set;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Prototype main class for the GMSClient.  Currently
+ * only used for testing.  Should not be used for production
+ * work.
+ */
+public class GMSClientMain implements PrivilegedAction<Object>
+{
+
+    private static Logger log = Logger.getLogger(GMSClientMain.class);
+
+    public static final String ARG_ADD_MEMBER = "add-member";
+    public static final String ARG_CREATE_GROUP = "create";
+    public static final String ARG_GET_GROUP = "get";
+    public static final String ARG_DELETE_GROUP = "delete";
+
+    public static final String ARG_USERID = "userid";
+    public static final String ARG_GROUP = "group";
+
+    public static final String ARG_HELP = "help";
+    public static final String ARG_VERBOSE = "verbose";
+    public static final String ARG_DEBUG = "debug";
+    public static final String ARG_H = "h";
+    public static final String ARG_V = "v";
+    public static final String ARG_D = "d";
+
+    private GMSClient client;
+    private ArgumentMap argMap;
+
+    private GMSClientMain()
+    {
+        RegistryClient regClient = new RegistryClient();
+        URL acURL = null;
+        try
+        {
+            acURL = regClient.getServiceURL(new URI("ivo://cadc.nrc.ca/canfargms"), "https");
+        }
+        catch (Exception e)
+        {
+            log.error("FAIL", e);
+        }
+        log.info("GMS service URL: " + acURL);
+        client = new GMSClient(acURL.toString());
+    }
+
+    public static void main(String[] args)
+    {
+        ArgumentMap argMap = new ArgumentMap(args);
+
+        if (argMap.isSet(ARG_HELP) || argMap.isSet(ARG_H))
+        {
+            usage();
+            System.exit(0);
+        }
+
+        // Set debug mode
+        if (argMap.isSet(ARG_DEBUG) || argMap.isSet(ARG_D))
+        {
+            Log4jInit.setLevel("ca.nrc.cadc.ac.client", Level.DEBUG);
+            Log4jInit.setLevel("ca.nrc.cadc.net", Level.DEBUG);
+        }
+        else if (argMap.isSet(ARG_VERBOSE) || argMap.isSet(ARG_V))
+        {
+            Log4jInit.setLevel("ca.nrc.cadc.ac.client", Level.INFO);
+        }
+        else
+            Log4jInit.setLevel("ca", Level.WARN);
+
+        GMSClientMain main = new GMSClientMain();
+        main.argMap = argMap;
+
+        Subject subject = CertCmdArgUtil.initSubject(argMap, true);
+
+        Object response = null;
+
+        if (subject != null)
+            response = Subject.doAs(subject, main);
+        else
+            response = main.run();
+
+        log.debug("Response: " + response);
+    }
+
+    private String getCommand()
+    {
+        if (argMap.isSet(ARG_ADD_MEMBER))
+            return ARG_ADD_MEMBER;
+        
+        if (argMap.isSet(ARG_CREATE_GROUP))
+            return ARG_CREATE_GROUP;
+        
+        if (argMap.isSet(ARG_GET_GROUP))
+            return ARG_GET_GROUP;
+        
+        if (argMap.isSet(ARG_DELETE_GROUP))
+            return ARG_DELETE_GROUP;
+
+        throw new IllegalArgumentException("No valid commands");
+    }
+
+    private static void usage()
+    {
+        System.out.println("--add-member --group=<g> --userid=<u>");
+        System.out.println("--create --group=<g>");
+        System.out.println("--get --group=<g>");
+        System.out.println("--delete --group=<g>");
+    }
+
+    @Override
+    public Object run()
+    {
+        try
+        {
+            String command = getCommand();
+
+            if (command.equals(ARG_ADD_MEMBER))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                String userID = argMap.getValue(ARG_USERID);
+
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+
+                if (userID == null)
+                    throw new IllegalArgumentException("No userid specified");
+
+                client.addUserMember(group, new HttpPrincipal(userID));
+            }
+            else if (command.equals(ARG_CREATE_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+                
+                AccessControlContext accessControlContext = AccessController.getContext();
+                Subject subject = Subject.getSubject(accessControlContext);
+                Set<X500Principal> principals = subject.getPrincipals(X500Principal.class);
+                X500Principal p = principals.iterator().next();
+                
+                Group g = new Group(group, new User(p));
+                g.getUserMembers().add(g.getOwner());
+                client.createGroup(g);
+            }
+            else if (command.equals(ARG_GET_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+             
+                Group g = client.getGroup(group);
+                System.out.println("found: " + g.getID());
+                System.out.println("\t" + g.description);
+                System.out.println("owner: " + g.getOwner());
+                
+                for (User u : g.getUserAdmins())
+                    System.out.println("admin: " + u);
+                
+                for (Group ga : g.getGroupAdmins())
+                    System.out.println("admin: " + ga);
+                
+                for (User u : g.getUserMembers())
+                    System.out.println("member: " + u);
+                
+                for (Group gm : g.getGroupMembers())
+                    System.out.println("member: " + gm);
+                
+            }
+            else if (command.equals(ARG_DELETE_GROUP))
+            {
+                String group = argMap.getValue(ARG_GROUP);
+                if (group == null)
+                    throw new IllegalArgumentException("No group specified");
+             
+                client.deleteGroup(group);
+            }
+
+            return null;
+        }
+        catch (Throwable t)
+        {
+            log.error("ERROR", t);
+            return t;
+        }
+    }
+}
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 2da9b1e8948109ca172b198bf0e96965b54b6a7e..9bf89c7fff88d9ef792fbe0eaf873cc088a53e38 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
@@ -97,48 +97,14 @@ import static org.easymock.EasyMock.*;
 
 public class GMSClientTest
 {
-    
+
     private static final Logger log = Logger.getLogger(GMSClientTest.class);
-    
+
     public 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()
     {
@@ -148,7 +114,7 @@ public class GMSClientTest
             HttpPrincipal userID = new HttpPrincipal("test");
             HttpPrincipal userID2 = new HttpPrincipal("test2");
             subject.getPrincipals().add(userID);
-            
+
             RegistryClient regClient = new RegistryClient();
             URL baseURL = regClient.getServiceURL(new URI(AC.GMS_SERVICE_URI),
                                                   "https");
@@ -159,10 +125,10 @@ public class GMSClientTest
             Assert.assertFalse(client.userIsSubject(null, subject));
             Assert.assertFalse(client.userIsSubject(userID2, subject));
             Assert.assertTrue(client.userIsSubject(userID, subject));
-            
+
             HttpPrincipal userID3 = new HttpPrincipal("test3");
             subject.getPrincipals().add(userID3);
-            
+
             Assert.assertTrue(client.userIsSubject(userID, subject));
             Assert.assertFalse(client.userIsSubject(userID2, subject));
             Assert.assertTrue(client.userIsSubject(userID3, subject));
@@ -173,7 +139,7 @@ public class GMSClientTest
             Assert.fail("Unexpected exception: " + t.getMessage());
         }
     }
-    
+
     @Test
     public void testGroupCaching()
     {
@@ -182,7 +148,7 @@ public class GMSClientTest
             Subject subject = new Subject();
             final HttpPrincipal test1UserID = new HttpPrincipal("test");
             subject.getPrincipals().add(test1UserID);
-            
+
             RegistryClient regClient = new RegistryClient();
             URL baseURL = regClient.getServiceURL(new URI(AC.GMS_SERVICE_URI),
                     "https");
@@ -194,37 +160,48 @@ public class GMSClientTest
                     public Object run() throws Exception
                     {
 
-                        List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
+                        List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER, true);
                         Assert.assertNull("Cache should be null", initial);
 
+                        // add single group as isMember might do
+                        Group group0 = new Group("0");
+                        client.addCachedGroup(test1UserID, group0, Role.MEMBER);
+                        List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER, true);
+                        Assert.assertNull("Cache should be null", actual);
+
+                        Group g = client.getCachedGroup(test1UserID, "0", Role.MEMBER);
+                        Assert.assertNotNull("cached group from incomplete cache", g);
+
+                        // add all groups like getMemberships might do
                         List<Group> expected = new ArrayList<Group>();
                         Group group1 = new Group("1");
                         Group group2 = new Group("2");
+                        expected.add(group0);
                         expected.add(group1);
                         expected.add(group2);
 
                         client.setCachedGroups(test1UserID, expected, Role.MEMBER);
 
-                        List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
+                        actual = client.getCachedGroups(test1UserID, Role.MEMBER, true);
                         Assert.assertEquals("Wrong cached groups", expected, actual);
-                        
+
                         // check against another role
-                        actual = client.getCachedGroups(test1UserID, Role.OWNER);
+                        actual = client.getCachedGroups(test1UserID, Role.OWNER, true);
                         Assert.assertNull("Cache should be null", actual);
-                        
+
                         // check against another userid
                         final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
-                        actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
+                        actual = client.getCachedGroups(anotherUserID, Role.MEMBER, true);
                         Assert.assertNull("Cache should be null", actual);
 
                         return null;
                     }
                 });
-            
+
             subject = new Subject();
             final HttpPrincipal test2UserID = new HttpPrincipal("test2");
             subject.getPrincipals().add(test2UserID);
-            
+
             // do the same but as a different user
             Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
                     {
@@ -232,7 +209,7 @@ public class GMSClientTest
                         public Object run() throws Exception
                         {
 
-                            List<Group> initial = client.getCachedGroups(test2UserID, Role.MEMBER);
+                            List<Group> initial = client.getCachedGroups(test2UserID, Role.MEMBER, true);
                             Assert.assertNull("Cache should be null", initial);
 
                             List<Group> expected = new ArrayList<Group>();
@@ -243,16 +220,16 @@ public class GMSClientTest
 
                             client.setCachedGroups(test2UserID, expected, Role.MEMBER);
 
-                            List<Group> actual = client.getCachedGroups(test2UserID, Role.MEMBER);
+                            List<Group> actual = client.getCachedGroups(test2UserID, Role.MEMBER, true);
                             Assert.assertEquals("Wrong cached groups", expected, actual);
-                            
+
                             // check against another role
-                            actual = client.getCachedGroups(test2UserID, Role.OWNER);
+                            actual = client.getCachedGroups(test2UserID, Role.OWNER, true);
                             Assert.assertNull("Cache should be null", actual);
-                            
+
                             // check against another userid
                             final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
-                            actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
+                            actual = client.getCachedGroups(anotherUserID, Role.MEMBER, true);
                             Assert.assertNull("Cache should be null", actual);
 
                             return null;
@@ -261,7 +238,7 @@ public class GMSClientTest
 
             // do the same without a subject
 
-            List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
+            List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER, true);
             Assert.assertNull("Cache should be null", initial);
 
             List<Group> newgroups = new ArrayList<Group>();
@@ -272,7 +249,7 @@ public class GMSClientTest
 
             client.setCachedGroups(test1UserID, newgroups, Role.MEMBER);
 
-            List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
+            List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER, true);
             Assert.assertNull("Cache should still be null", actual);
         }
         catch (Throwable t)