diff --git a/projects/cadcAccessControl-Server/Dependencies.txt b/projects/cadcAccessControl-Server/Dependencies.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7ef8710a2be2a815c1c2b2bfe670a2f8603a2e57
--- /dev/null
+++ b/projects/cadcAccessControl-Server/Dependencies.txt
@@ -0,0 +1,15 @@
+JAR files required for the OpenCADC cadcAccessControl-Server project
+====================================================================
+
+Name in build.xml            Versioned Name          Project URL
+-----------------            --------------          -----------
+jdom.jar                     jdom-1.1                http://www.jdom.org
+log4j.jar                    log4j-1.2.15            http://logging.apache.org
+xerces.jar                   xerces-2_9_1            http://xerces.apache.org
+servlet-api.jar              apache-tomcat-5.5.20    http://tomcat.apache.org
+jdom2jar                     jdom-2.0.5              http://www.jdom.org
+cadcRegistryClient.jar                               http://code.google.com/p/opencadc
+cadcUtil.jar                                         http://code.google.com/p/opencadc
+cadcAccessControl.jar                                http://code.google.com/p/opencadc
+cadcUWS.jar                                          http://code.google.com/p/opencadc
+cadcLog.jar                                          http://code.google.com/p/opencadc
\ No newline at end of file
diff --git a/projects/cadcAccessControl-Server/PluginFactory.properties b/projects/cadcAccessControl-Server/PluginFactory.properties
new file mode 100644
index 0000000000000000000000000000000000000000..3c47cbb416c913936ebe91192c0c91a34e535e44
--- /dev/null
+++ b/projects/cadcAccessControl-Server/PluginFactory.properties
@@ -0,0 +1,9 @@
+## commented out values are the defaults, shown as examples
+## to customise behaviour, subclass the specified class and
+## change the configuration here
+
+## UserPersistence implementation
+ca.nrc.cadc.ac.server.UserPersistence = ca.nrc.cadc.ac.server.ldap.LdapUserPersistence
+
+## GroupPersistence implementation
+ca.nrc.cadc.ac.server.GroupPersistence = ca.nrc.cadc.ac.server.ldap.LdapGroupPersistence
\ No newline at end of file
diff --git a/projects/cadcAccessControl-Server/build.xml b/projects/cadcAccessControl-Server/build.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f30a6185a9e20cee5cc39941fc1f563c830e3410
--- /dev/null
+++ b/projects/cadcAccessControl-Server/build.xml
@@ -0,0 +1,148 @@
+<!--
+************************************************************************
+*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
+*
+*  (c) 2009.                            (c) 2009.
+*  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 $
+*
+************************************************************************
+-->
+
+
+<!DOCTYPE project>
+<project name="cadcAccessControl-Server" default="build" basedir=".">
+    <property environment="env"/>
+    <property file="local.build.properties" />
+
+    <!-- site-specific build properties or overrides of values in opencadc.properties -->
+    <property file="${env.CADC_PREFIX}/etc/local.properties" />
+
+    <!-- site-specific targets, e.g. install, cannot duplicate those in opencadc.targets.xml -->
+    <import file="${env.CADC_PREFIX}/etc/local.targets.xml" optional="true" />
+
+    <!-- default properties and targets -->
+    <property file="${env.CADC_PREFIX}/etc/opencadc.properties" />
+    <import file="${env.CADC_PREFIX}/etc/opencadc.targets.xml"/>
+
+    <!-- developer convenience: place for extra targets and properties -->
+    <import file="extras.xml" optional="true" />
+
+    <property name="project" value="cadcAccessControl-Server" />
+
+    <property name="cadcAccessControl"   value="${lib}/cadcAccessControl.jar" />
+    <property name="cadcLog"             value="${lib}/cadcLog.jar" />
+    <property name="cadcRegistry"        value="${lib}/cadcRegistryClient.jar" />
+    <property name="cadcUtil"            value="${lib}/cadcUtil.jar" />
+    <property name="cadcUWS"             value="${lib}/cadcUWS.jar" />
+    <property name="wsUtil"              value="${lib}/wsUtil.jar" />
+
+    <property name="javacsv"             value="${ext.lib}/javacsv.jar" />
+    <property name="jdom2"               value="${ext.lib}/jdom2.jar" />
+    <property name="log4j"               value="${ext.lib}/log4j.jar" />
+    <property name="servlet"             value="${ext.lib}/servlet-api.jar" />
+    <property name="unboundid"           value="${ext.lib}/unboundid-ldapsdk-se.jar" />
+    <property name="xerces"              value="${ext.lib}/xerces.jar" />
+
+    <property name="jars" value="${javacsv}:${jdom2}:${log4j}:${servlet}:${unboundid}:${xerces}:${cadcAccessControl}:${cadcLog}:${cadcRegistry}:${cadcUtil}:${cadcUWS}:${wsUtil}" />
+
+    <target name="build" depends="compile">
+        <jar jarfile="${build}/lib/${project}.jar"
+             basedir="${build}/class"
+             update="no">
+            <include name="ca/nrc/cadc/**" />
+        </jar>
+    </target>
+
+    <!-- JAR files needed to run the test suite -->
+    <property name="gson"           value="${ext.lib}/gson.jar" />
+    <property name="easyMock"       value="${ext.dev}/easymock.jar" />
+    <property name="junit"          value="${ext.dev}/junit.jar" />
+    <property name="xmlunit"        value="${ext.dev}/xmlunit.jar" />
+    <property name="xerces"         value="${ext.lib}/xerces.jar" />
+    <property name="cglib"          value="${ext.dev}/cglib.jar" />
+    <property name="objenesis"      value="${ext.dev}/objenesis.jar" />
+    <property name="asm"            value="${ext.dev}/asm.jar" />
+
+    <property name="testingJars"    value="${jars}:${gson}:${easyMock}:${junit}:${xmlunit}:${xerces}:${cglib}:${asm}:${objenesis}" />
+
+    <target name="resources">
+        <copy todir="${build}/class">
+            <fileset dir="config">
+                <include name="**.properties" />
+            </fileset>
+        </copy>
+    </target>
+
+    <!--<target name="test" depends="compile,compile-test,resources">-->
+        <!--<echo message="Running test suite..." />-->
+        <!--<junit printsummary="yes" haltonfailure="yes" fork="yes">-->
+            <!--<classpath>-->
+                <!--<pathelement path="${build}/class"/>-->
+                <!--<pathelement path="${build}/test/class"/>-->
+                <!--<pathelement path="${testingJars}"/>-->
+            <!--</classpath>-->
+            <!--<test name="ca.nrc.cadc.ac.server.ldap.LdapGroupDAOTest" />-->
+            <!--<formatter type="plain" usefile="false" />-->
+        <!--</junit>-->
+    <!--</target>-->
+
+</project>
diff --git a/projects/cadcAccessControl-Server/config/.dbrc_example b/projects/cadcAccessControl-Server/config/.dbrc_example
new file mode 100644
index 0000000000000000000000000000000000000000..ced094848f172ce8628940a22568afa4efb3d6be
--- /dev/null
+++ b/projects/cadcAccessControl-Server/config/.dbrc_example
@@ -0,0 +1,2 @@
+#server	proxyuser proxyUserDN password driver serverURL
+<server hostname> <proxyUser in LdapConfig.properties> <proxyUserLdapDN> <password> N/A N/A
diff --git a/projects/cadcAccessControl-Server/config/LdapConfig.properties b/projects/cadcAccessControl-Server/config/LdapConfig.properties
new file mode 100644
index 0000000000000000000000000000000000000000..5eb874d802b890852e308e6880946751513437dc
--- /dev/null
+++ b/projects/cadcAccessControl-Server/config/LdapConfig.properties
@@ -0,0 +1,7 @@
+# This are the configuration fields required by the Ldap
+server = <name of server> 
+port = <389 or 636>
+proxyUser = <name of proxy user>
+usersDn = <DN of users branch>
+groupsDn = <DN of groups branch>
+adminGroupsDn = <DN of admin groups>
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java
new file mode 100755
index 0000000000000000000000000000000000000000..bdfa4e05c79516396085e5ffb90d031dc2d7c3e2
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/GroupPersistence.java
@@ -0,0 +1,178 @@
+/*
+ ************************************************************************
+ *******************  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.server;
+
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
+
+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.net.TransientException;
+
+public abstract interface GroupPersistence<T extends Principal>
+{
+    /**
+     * Get all group names.
+     * 
+     * @return A collection of strings.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public Collection<String> getGroupNames()
+            throws TransientException, AccessControlException;
+    
+    /**
+     * Get the group with the given Group ID.
+     *
+     * @param groupID The Group ID.
+     * 
+     * @return A Group instance
+     *
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract Group getGroup(String groupID)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException;
+
+    /**
+     * Creates the group.
+     *
+     * @param group The group to create
+     * 
+     * @return created group
+     *
+     * @throws GroupAlreadyExistsException If a group with the same ID already
+     *                                     exists.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     * @throws UserNotFoundException If owner or a member not valid user.
+     * @throws GroupNotFoundException if one of the groups in group members or
+     * group admins does not exist in the server.
+     */
+    public abstract Group addGroup(Group group)
+        throws GroupAlreadyExistsException, TransientException,
+               AccessControlException, UserNotFoundException, 
+               GroupNotFoundException;
+
+    /**
+     * Deletes the group.
+     *
+     * @param groupID The Group ID.
+     *
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract void deleteGroup(String groupID)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException;
+
+    /**
+     * Modify the given group.
+     *
+     * @param group The group to update.
+     * 
+     * @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.
+     * @throws UserNotFoundException If owner or group members not valid users.
+     */
+    public abstract Group modifyGroup(Group group)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException, UserNotFoundException;
+
+    /**
+     * Obtain a Collection of Groups that fit the given query.
+     *
+     * @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 the query, or empty Collection.
+     *         Never null.
+     *
+     * @throws UserNotFoundException If owner or group members not valid users.
+     * @throws ca.nrc.cadc.ac.GroupNotFoundException
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract Collection<Group> getGroups(T userID, Role role, 
+                                                String groupID)
+        throws UserNotFoundException, GroupNotFoundException,
+               TransientException, AccessControlException;
+    
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/PluginFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/PluginFactory.java
new file mode 100755
index 0000000000000000000000000000000000000000..caa18b20c505d98444c4b428ca8a8a9dad7b0aba
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/PluginFactory.java
@@ -0,0 +1,165 @@
+/*
+ ************************************************************************
+ *******************  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.server;
+
+import ca.nrc.cadc.ac.server.ldap.LdapGroupPersistence;
+import ca.nrc.cadc.ac.server.ldap.LdapUserPersistence;
+import java.net.URL;
+import java.security.Principal;
+import java.util.Properties;
+import java.util.Set;
+import org.apache.log4j.Logger;
+
+public class PluginFactory
+{
+    private static final Logger log = Logger.getLogger(PluginFactory.class);
+
+    private static final String CONFIG = PluginFactory.class.getSimpleName() + ".properties";
+    private Properties config;
+
+    public PluginFactory()
+    {
+        init();
+    }
+
+    @Override
+    public String toString()
+    {
+        return getClass().getName() + "[" + config.entrySet().size() + "]";
+    }
+
+    private void init()
+    {
+        config = new Properties();
+        URL url = null;
+        try
+        {
+            url = PluginFactory.class.getClassLoader().getResource(CONFIG);
+            if (url != null)
+            {
+                config.load(url.openStream());
+            }
+        }
+        catch (Exception ex)
+        {
+            throw new RuntimeException("failed to read " + CONFIG + " from " + url, ex);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Principal> GroupPersistence<T> getGroupPersistence()
+    {
+        GroupPersistence<T> ret = null;
+        String name = GroupPersistence.class.getName();
+        String cname = config.getProperty(name);
+        if (cname == null)
+        {
+            ret = new LdapGroupPersistence<T>();
+        }
+        else
+        {
+            try
+            {
+                Class<?> c = Class.forName(cname);
+                ret = (GroupPersistence<T>) c.newInstance();
+            }
+            catch (Exception ex)
+            {
+                throw new RuntimeException("config error: failed to create GroupPersistence " + cname, ex);
+            }
+        }
+        return ret;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <T extends Principal> UserPersistence<T> getUserPersistence()
+    {
+        UserPersistence<T> ret = null;
+        String name = UserPersistence.class.getName();
+        String cname = config.getProperty(name);
+        if (cname == null)
+        {
+            ret = new LdapUserPersistence<T>();
+        }
+        else
+        {
+            try
+            {
+                Class<?> c = Class.forName(cname);
+                ret = (UserPersistence<T>) c.newInstance();
+            }
+            catch (Exception ex)
+            {
+                throw new RuntimeException("config error: failed to create UserPersistence " + cname, ex);
+            }
+        }
+        return ret;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/RequestValidator.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/RequestValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..f35fef417dbc5f90ca550c1013783e649c52f043
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/RequestValidator.java
@@ -0,0 +1,172 @@
+/*
+ ************************************************************************
+ *******************  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.server;
+
+import java.security.Principal;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.Role;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.uws.Parameter;
+import ca.nrc.cadc.uws.ParameterUtil;
+
+/**
+ * Request Validator. This class extracts and validates the ID, TYPE, ROLE
+ * and GURI parameters.
+ *
+ */
+public class RequestValidator
+{
+    private static final Logger log = Logger.getLogger(RequestValidator.class);
+    
+    private Principal principal;
+    private Role role;
+    private String groupID;
+    
+    public RequestValidator() { }
+
+    private void clear()
+    {
+        this.principal = null;
+        this.role = null;
+        this.groupID = null;
+    }
+    
+    public void validate(List<Parameter> paramList)
+    {
+        clear();
+        if (paramList == null || paramList.isEmpty())
+        {
+            throw new IllegalArgumentException(
+                    "Missing required parameters: ID, IDTYPE, ROLE");
+        }
+
+        // ID
+        String param = ParameterUtil.findParameterValue("ID", paramList);
+        if (param == null || param.trim().isEmpty())
+        {
+            throw new IllegalArgumentException(
+                    "ID parameter required but not found");
+        }
+        String userID = param.trim();
+        log.debug("ID: " + userID);
+
+        // TYPE
+        param = ParameterUtil.findParameterValue("IDTYPE", paramList);
+        if (param == null || param.trim().isEmpty())
+        {
+            throw new IllegalArgumentException(
+                    "IDTYPE parameter required but not found");
+        }
+        
+        principal = 
+            AuthenticationUtil.createPrincipal(userID, 
+                                               param.trim());
+        log.debug("TYPE: " + param.trim());
+        
+        // ROLE
+        param = ParameterUtil.findParameterValue("ROLE", paramList);
+        if (param == null || param.trim().isEmpty())
+        {
+            throw new IllegalArgumentException(
+                    "ROLE parameter required but not found");
+        }
+        this.role = Role.toValue(param);
+        log.debug("ROLE: " + role);
+        
+        // GROUPID
+        param = ParameterUtil.findParameterValue("GROUPID", paramList);
+        if (param != null)
+        {
+            if (param.isEmpty())
+                throw new IllegalArgumentException(
+                        "GROUPID parameter specified without a value");
+            this.groupID = param.trim();
+        }
+        log.debug("GROUPID: " + groupID);
+    }
+    
+    public Principal getPrincipal()
+    {
+        return principal;
+    }
+
+    public Role getRole()
+    {
+        return role;
+    }
+    
+    public String getGroupID()
+    {
+        return groupID;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
new file mode 100755
index 0000000000000000000000000000000000000000..703a3d62a170221b4a4ef875e4596c4ffc8880f9
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/UserPersistence.java
@@ -0,0 +1,128 @@
+/*
+ ************************************************************************
+ *******************  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.server;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.net.TransientException;
+import com.unboundid.ldap.sdk.DN;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
+
+public abstract interface UserPersistence<T extends Principal>
+{
+    /**
+     * Get the user specified by userID.
+     *
+     * @param userID The userID.
+     *
+     * @return User instance.
+     * 
+     * @throws UserNotFoundException when the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract User<T> getUser(T userID)
+        throws UserNotFoundException, TransientException, 
+               AccessControlException;
+    
+    /**
+     * Get all groups the user specified by userID belongs to.
+     * 
+     * @param userID The userID.
+     * @param isAdmin return only admin Groups when true, else return non-admin
+     *                Groups.
+     * 
+     * @return Collection of group DN.
+     * 
+     * @throws UserNotFoundException  when the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract Collection<DN> getUserGroups(T userID, boolean isAdmin)
+        throws UserNotFoundException, TransientException,
+               AccessControlException;
+    
+    /**
+     * Check whether the user is a member of the group.
+     *
+     * @param userID The userID.
+     * @param groupID The groupID.
+     *
+     * @return true or false
+     *
+     * @throws UserNotFoundException If the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public abstract boolean isMember(T userID, String groupID)
+        throws UserNotFoundException, TransientException,
+               AccessControlException;
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapConfig.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapConfig.java
new file mode 100755
index 0000000000000000000000000000000000000000..42995612395ededc619d730d5a1441c31aea0900
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapConfig.java
@@ -0,0 +1,307 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.db.ConnectionConfig;
+import ca.nrc.cadc.db.DBConfig;
+import ca.nrc.cadc.util.MultiValuedProperties;
+import ca.nrc.cadc.util.PropertiesReader;
+import ca.nrc.cadc.util.StringUtil;
+
+/**
+ * Reads and stores the LDAP configuration information. The information 
+ * 
+ * @author adriand
+ *
+ */
+public class LdapConfig
+{
+    private static final Logger logger = Logger.getLogger(LdapConfig.class);
+
+    public static final String CONFIG = LdapConfig.class.getSimpleName() + 
+                                        ".properties";
+    public static final String LDAP_SERVER = "server";
+    public static final String LDAP_PORT = "port";
+    public static final String LDAP_SERVER_PROXY_USER = "proxyUser";
+    public static final String LDAP_USERS_DN = "usersDn";
+    public static final String LDAP_GROUPS_DN = "groupsDn";
+    public static final String LDAP_ADMIN_GROUPS_DN  = "adminGroupsDn";
+
+    private final static int SECURE_PORT = 636;
+
+    private String usersDN;
+    private String groupsDN;
+    private String adminGroupsDN;
+    private String server;
+    private int port;
+    private String proxyUserDN;
+    private String proxyPasswd;
+    
+    public String getProxyUserDN()
+    {
+        return proxyUserDN;
+    }
+
+    public String getProxyPasswd()
+    {
+        return proxyPasswd;
+    }
+
+    public static LdapConfig getLdapConfig()
+    {
+        return getLdapConfig(CONFIG);
+    }
+
+    public static LdapConfig getLdapConfig(final String ldapProperties)
+    {
+        PropertiesReader pr = new PropertiesReader(ldapProperties);
+        
+        MultiValuedProperties config = pr.getAllProperties();
+        
+        if (config.keySet() == null)
+        {
+            throw new RuntimeException("failed to read any LDAP property ");
+        }
+        
+        List<String> prop = config.getProperty(LDAP_SERVER);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + 
+                                       LDAP_SERVER);
+        }
+        String server = prop.get(0);
+
+        prop = config.getProperty(LDAP_PORT);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + LDAP_PORT);
+        }
+        int port = Integer.valueOf(prop.get(0));
+        
+        prop = config.getProperty(LDAP_SERVER_PROXY_USER);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + 
+                    LDAP_SERVER_PROXY_USER);
+        }
+        String ldapProxy = prop.get(0);
+        
+        prop = config.getProperty(LDAP_USERS_DN);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + 
+                                       LDAP_USERS_DN);
+        }
+        String ldapUsersDn = prop.get(0);
+
+        prop = config.getProperty(LDAP_GROUPS_DN);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + 
+                                       LDAP_GROUPS_DN);
+        }
+        String ldapGroupsDn = prop.get(0);
+        
+        prop = config.getProperty(LDAP_ADMIN_GROUPS_DN);
+        if ((prop == null) || (prop.size() != 1))
+        {
+            throw new RuntimeException("failed to read property " + 
+                                       LDAP_ADMIN_GROUPS_DN);
+        }
+        String ldapAdminGroupsDn = prop.get(0);
+        
+        DBConfig dbConfig;
+        try
+        {
+            dbConfig = new DBConfig();
+        } 
+        catch (FileNotFoundException e)
+        {
+            throw new RuntimeException("failed to find .dbrc file ");
+        } 
+        catch (IOException e)
+        {
+            throw new RuntimeException("failed to read .dbrc file ");
+        }
+        ConnectionConfig cc = dbConfig.getConnectionConfig(server, ldapProxy);
+        if ( (cc == null) || (cc.getUsername() == null) || (cc.getPassword() == null))
+        {
+            throw new RuntimeException("failed to find connection info in ~/.dbrc");
+        }
+        
+        return new LdapConfig(server, Integer.valueOf(port), cc.getUsername(), 
+                              cc.getPassword(), ldapUsersDn, ldapGroupsDn,
+                              ldapAdminGroupsDn);
+    }
+    
+
+    public LdapConfig(String server, int port, String proxyUserDN, 
+                      String proxyPasswd, String usersDN, String groupsDN,
+                      String adminGroupsDN)
+    {
+        if (!StringUtil.hasText(server))
+        {
+            throw new IllegalArgumentException("Illegal LDAP server name");
+        }
+        if (port < 0)
+        {
+            throw new IllegalArgumentException("Illegal LDAP server port: " + 
+                                               port);
+        }
+        if (!StringUtil.hasText(proxyUserDN))
+        {
+            throw new IllegalArgumentException("Illegal Admin DN");
+        }
+        if (!StringUtil.hasText(proxyPasswd))
+        {
+            throw new IllegalArgumentException("Illegal Admin password");
+        }
+        if (!StringUtil.hasText(usersDN))
+        {
+            throw new IllegalArgumentException("Illegal users LDAP DN");
+        }
+        if (!StringUtil.hasText(groupsDN))
+        {
+            throw new IllegalArgumentException("Illegal groups LDAP DN");
+        }
+        if (!StringUtil.hasText(adminGroupsDN))
+        {
+            throw new IllegalArgumentException("Illegal admin groups LDAP DN");
+        }
+        
+        this.server = server;
+        this.port = port;
+        this.proxyUserDN = proxyUserDN;
+        this.proxyPasswd = proxyPasswd;
+        this.usersDN = usersDN;
+        this.groupsDN = groupsDN;
+        this.adminGroupsDN = adminGroupsDN;
+        logger.debug(toString());
+    }
+
+    public String getUsersDN()
+    {
+        return this.usersDN;
+    }
+
+    public String getGroupsDN()
+    {
+        return this.groupsDN;
+    }
+    
+    public String getAdminGroupsDN()
+    {
+        return this.adminGroupsDN;
+    }
+
+    public String getServer()
+    {
+        return this.server;
+    }
+
+    public int getPort()
+    {
+        return this.port;
+    }
+
+    public boolean isSecure()
+    {
+        return getPort() == SECURE_PORT;
+    }
+
+    public String getAdminUserDN()
+    {
+        return this.proxyUserDN;
+    }
+
+    public String getAdminPasswd()
+    {
+        return this.proxyPasswd;
+    }
+
+    public String toString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append("server = ");
+        sb.append(server);
+        sb.append(" port = ");
+        sb.append(port);
+        sb.append(" proxyUserDN = ");
+        sb.append(proxyUserDN);
+        sb.append(" proxyPasswd = ");
+        sb.append(proxyPasswd);
+        return sb.toString(); 
+    }
+}
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
new file mode 100755
index 0000000000000000000000000000000000000000..abd19b24bde0eda15142be362587d4b22241b26a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapDAO.java
@@ -0,0 +1,257 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.util.Set;
+
+import com.unboundid.ldap.sdk.*;
+import com.unboundid.util.ssl.*;
+
+import ca.nrc.cadc.auth.*;
+import ca.nrc.cadc.net.TransientException;
+
+
+public abstract class LdapDAO
+{
+    private LDAPConnection conn;
+
+    LdapConfig config;
+    DN subjDN = null;
+
+    public LdapDAO(LdapConfig config)
+    {
+        if (config == null)
+        {
+            throw new IllegalArgumentException("LDAP config required");
+        }
+        this.config = config;
+    }
+
+    public void close()
+    {
+        if (conn != null)
+        {
+            conn.close();
+        }
+    }
+
+    protected LDAPConnection getConnection()
+            throws LDAPException, AccessControlException
+    {
+        if (conn == null)
+        {
+            conn = new LDAPConnection(getSocketFactory(), config.getServer(),
+                                      config.getPort());
+            conn.bind(config.getAdminUserDN(), config.getAdminPasswd());
+        }
+
+        return conn;
+    }
+
+    private SocketFactory getSocketFactory()
+    {
+        final SocketFactory socketFactory;
+
+        if (config.isSecure())
+        {
+            socketFactory = createSSLSocketFactory();
+        }
+        else
+        {
+            socketFactory = SocketFactory.getDefault();
+        }
+
+        return socketFactory;
+    }
+
+    private SSLSocketFactory createSSLSocketFactory()
+    {
+        try
+        {
+            return new com.unboundid.util.ssl.SSLUtil().
+                    createSSLSocketFactory();
+        }
+        catch (GeneralSecurityException e)
+        {
+            throw new RuntimeException("Unexpected error.", e);
+        }
+    }
+
+    protected DN getSubjectDN() throws LDAPException
+    {
+        if (subjDN == null)
+        {
+            Subject callerSubject =
+                    Subject.getSubject(AccessController.getContext());
+            if (callerSubject == null)
+            {
+                throw new AccessControlException("Caller not authenticated.");
+            }
+
+            Set<Principal> principals = callerSubject.getPrincipals();
+            if (principals.isEmpty())
+            {
+                throw new AccessControlException("Caller not authenticated.");
+            }
+
+            String ldapField = null;
+            for (Principal p : principals)
+            {
+                if (p instanceof HttpPrincipal)
+                {
+                    ldapField = "(uid=" + p.getName() + ")";
+                    break;
+                }
+                if (p instanceof NumericPrincipal)
+                {
+                    ldapField = "(entryid=" + p.getName() + ")";
+                    break;
+                }
+                if (p instanceof X500Principal)
+                {
+                    ldapField = "(distinguishedname=" + p.getName() + ")";
+                    break;
+                }
+                if (p instanceof OpenIdPrincipal)
+                {
+                    ldapField = "(openid=" + p.getName() + ")";
+                    break;
+                }
+            }
+
+            if (ldapField == null)
+            {
+                throw new AccessControlException("Identity of caller unknown.");
+            }
+
+            SearchResult searchResult =
+                    getConnection().search(config.getUsersDN(), SearchScope.ONE,
+                                           ldapField, "entrydn");
+
+            if (searchResult.getEntryCount() < 1)
+            {
+                throw new AccessControlException(
+                        "No LDAP account when search with rule " + ldapField);
+            }
+
+            subjDN = (searchResult.getSearchEntries().get(0))
+                    .getAttributeValueAsDN("entrydn");
+        }
+        return subjDN;
+    }
+
+    /**
+     * Checks the Ldap result code, and if the result is not SUCCESS,
+     * throws an appropriate exception. This is the place to decide on
+     * mapping between ldap errors and exception types
+     *
+     * @param code          The code returned from an LDAP request.
+     * @throws TransientException
+     */
+    protected static void checkLdapResult(ResultCode code)
+            throws TransientException
+    {
+        if (code == ResultCode.INSUFFICIENT_ACCESS_RIGHTS)
+        {
+            throw new AccessControlException("Not authorized ");
+        }
+        else if (code == ResultCode.INVALID_CREDENTIALS)
+        {
+            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 ");
+        }
+        else if (code == ResultCode.BUSY ||
+                 code == ResultCode.CONNECT_ERROR)
+        {
+            throw new TransientException("Connection problems ");
+        }
+        else
+        {
+            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
new file mode 100755
index 0000000000000000000000000000000000000000..70ee0595f868c4805fda36310ff6cbd9b9278a5b
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAO.java
@@ -0,0 +1,1033 @@
+/*
+ ************************************************************************
+ *******************  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.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;
+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.net.TransientException;
+import ca.nrc.cadc.util.StringUtil;
+
+import com.unboundid.ldap.sdk.AddRequest;
+import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.DN;
+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 LdapUserDAO<T> userPersist;
+
+    public LdapGroupDAO(LdapConfig config, LdapUserDAO<T> userPersist)
+    {
+        super(config);
+        if (userPersist == null)
+        {
+            throw new IllegalArgumentException(
+                    "User persistence instance required");
+        }
+        this.userPersist = userPersist;
+    }
+
+    /**
+     * Persists a group.
+     * 
+     * @param group The group to create
+     * 
+     * @return created group
+     * 
+     * @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 
+     */
+    public Group addGroup(final Group group)
+        throws GroupAlreadyExistsException, TransientException,
+               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");
+        }
+
+        try
+        {
+            Group newGroup = reactivateGroup(group);
+            if ( newGroup != null)
+            {
+                return newGroup;
+            }
+            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(), 
+                                             group.getGroupMembers());
+                LdapDAO.checkLdapResult(result.getResultCode());
+                
+                // add group to admin groups tree
+                result = addGroup(getAdminGroupDN(group.getID()), 
+                                  group.getID(), ownerDN, 
+                                  group.description, 
+                                  group.getUserAdmins(), 
+                                  group.getGroupAdmins());
+                LdapDAO.checkLdapResult(result.getResultCode());
+                
+                try
+                {
+                    return getGroup(group.getID());
+                }
+                catch (GroupNotFoundException e)
+                {
+                    throw new RuntimeException("BUG: new group not found");
+                }
+            }
+        }
+        catch (LDAPException 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 Set<Group> groups)
+        throws UserNotFoundException, LDAPException, TransientException, 
+        AccessControlException, GroupNotFoundException
+    {
+        // add new group
+        List<Attribute> attributes = new ArrayList<Attribute>();
+        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));
+        }
+
+        List<String> members = new ArrayList<String>();
+        for (User<? extends Principal> userMember : users)
+        {
+            DN memberDN = this.userPersist.getUserDN(userMember);
+            members.add(memberDN.toNormalizedString());
+        }
+        for (Group groupMember : groups)
+        {
+            final String groupMemberID = groupMember.getID();
+            if (!checkGroupExists(groupMemberID))
+            {
+                throw new GroupNotFoundException(groupMemberID);
+            }
+            DN memberDN = getGroupDN(groupMemberID);
+            members.add(memberDN.toNormalizedString());
+        }
+        if (!members.isEmpty())
+        {
+            attributes.add(new Attribute("uniquemember", 
+                (String[]) members.toArray(new String[members.size()])));
+        }
+
+        AddRequest addRequest = new AddRequest(groupDN, attributes);
+        addRequest.addControl(
+                new ProxiedAuthorizationV2RequestControl(
+                        "dn:" + getSubjectDN().toNormalizedString()));
+
+        return getConnection().add(addRequest);
+    }
+    
+    
+    /**
+     * Checks whether group name available for the user or already in use.
+     * @param group
+     * @return activated group or null if group does not exists
+     * @throws AccessControlException
+     * @throws UserNotFoundException
+     * @throws GroupNotFoundException
+     * @throws TransientException
+     * @throws GroupAlreadyExistsException 
+     */
+    private Group reactivateGroup(final Group group)
+        throws AccessControlException, UserNotFoundException,
+        TransientException, GroupAlreadyExistsException
+    {
+        try
+        {
+            // check group name exists           
+            Filter filter = Filter.createEqualityFilter("cn", group.getID());
+
+            SearchRequest searchRequest = 
+                    new SearchRequest(
+                            getGroupDN(group.getID())
+                            .toNormalizedString(), SearchScope.SUB, filter, 
+                                      new String[] {"nsaccountlock"});
+
+            searchRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+                            getSubjectDN().toNormalizedString()));
+
+            SearchResultEntry searchResult = 
+                    getConnection().searchForEntry(searchRequest);
+            
+            if (searchResult == null)
+            {
+                return null;
+            }
+
+            if (searchResult.getAttributeValue("nsaccountlock") == null)
+            {
+                throw new 
+                GroupAlreadyExistsException("Group already exists " + group.getID());
+            }
+            
+            // activate group            
+            try
+            {
+                return modifyGroup(group, true);
+            } 
+            catch (GroupNotFoundException e)
+            {
+                throw new RuntimeException(
+                        "BUG: group to modify does not exist" + group.getID());
+            }          
+        } 
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+            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
+    {
+        try
+        {
+            Filter filter = Filter.createPresenceFilter("cn");
+            String [] attributes = new String[] {"cn", "nsaccountlock"};
+            
+            SearchRequest searchRequest = 
+                    new SearchRequest(config.getGroupsDN(), 
+                                      SearchScope.SUB, filter, attributes);
+    
+            SearchResult searchResult = null;
+            try
+            {
+                searchResult = getConnection().search(searchRequest);
+            }
+            catch (LDAPSearchException e)
+            {
+                if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
+                {
+                    logger.debug("Could not find groups root", e);
+                    throw new IllegalStateException("Could not find groups root");
+                }
+            }
+            
+            LdapDAO.checkLdapResult(searchResult.getResultCode());
+            List<String> groupNames = new ArrayList<String>();
+            for (SearchResultEntry next : searchResult.getSearchEntries())
+            {
+                if (!next.hasAttribute("nsaccountlock"))
+                {
+                    groupNames.add(next.getAttributeValue("cn"));
+                }
+            }
+            
+            return groupNames;
+        }
+        catch (LDAPException e1)
+        {
+            LdapDAO.checkLdapResult(e1.getResultCode());
+            throw new IllegalStateException("Unexpected exception: " + e1.getMatchedDN(), e1);
+        }
+        
+    }
+
+    /**
+     * Get the group with the given Group ID.
+     * 
+     * @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.
+     */
+    public Group getGroup(final String groupID)
+        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.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, 
+               AccessControlException
+    {
+        try
+        {
+            Filter filter = Filter.createEqualityFilter("cn", groupID);
+            
+            SearchRequest searchRequest = 
+                    new SearchRequest(groupDN.toNormalizedString(), 
+                                      SearchScope.SUB, filter, attributes);
+
+            searchRequest.addControl(
+                    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.getMember(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"))
+            {
+                ldapGroup.lastModified = 
+                        searchEntry.getAttributeValueAsDate("modifytimestamp");
+            }
+
+            if (withMembers)
+            {
+                if (searchEntry.getAttributeValues("uniquemember") != null)
+                {
+                    for (String member : searchEntry
+                            .getAttributeValues("uniquemember"))
+                    {
+                        DN memberDN = new DN(member);
+                        if (memberDN.isDescendantOf(config.getUsersDN(), false))
+                        {
+                            User<X500Principal> user;
+                            try
+                            {
+                                user = userPersist.getMember(memberDN);
+                            }
+                            catch (UserNotFoundException e)
+                            {
+                                throw new RuntimeException(
+                                    "BUG: group member not found");
+                            }
+                            ldapGroup.getUserMembers().add(user);
+                        }
+                        else if (memberDN.isDescendantOf(config.getGroupsDN(),
+                                                         false))
+                        {
+                            ldapGroup.getGroupMembers().add(new Group(
+                                memberDN.getRDNString().replace("cn=", "")));
+                        }
+                        else
+                        {
+                            throw new RuntimeException(
+                                "BUG: unknown member DN type: " + memberDN);
+                        }
+                    }
+                }
+            }
+            
+            return ldapGroup;
+        }
+        catch (LDAPException e1)
+        {
+            LdapDAO.checkLdapResult(e1.getResultCode());
+            throw new GroupNotFoundException("Not found " + groupID);
+        }
+    }
+
+    /**
+     * 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.
+     * @throws UserNotFoundException If owner or group members not valid users.
+     */
+    public Group modifyGroup(final Group group)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException, UserNotFoundException
+    {
+        getGroup(group.getID()); //group must exists first
+        return modifyGroup(group, false); 
+    }
+    
+    private Group modifyGroup(final Group group, boolean withActivate)
+        throws UserNotFoundException, TransientException,
+               AccessControlException, GroupNotFoundException
+    {
+        if (!group.getProperties().isEmpty())
+        {
+            throw new UnsupportedOperationException(
+                    "Support for groups properties not available");
+        }
+
+        List<Modification> mods = new ArrayList<Modification>();
+        List<Modification> adminMods = new ArrayList<Modification>();
+        if (withActivate)
+        {
+            mods.add(new Modification(ModificationType.DELETE, "nsaccountlock"));
+            adminMods.add(new Modification(ModificationType.DELETE, "nsaccountlock"));
+        }
+
+        if (group.description == null)
+        {
+            mods.add(new Modification(ModificationType.REPLACE, "description"));
+        }
+        else
+        {
+            mods.add(new Modification(ModificationType.REPLACE, "description", group.description));
+        }
+
+        List<String> newMembers = new ArrayList<String>();
+        for (User<?> member : group.getUserMembers())
+        {
+            DN memberDN = userPersist.getUserDN(member);
+            newMembers.add(memberDN.toNormalizedString());
+        }
+        for (Group gr : group.getGroupMembers())
+        {
+            if (!checkGroupExists(gr.getID()))
+            {
+                throw new GroupNotFoundException(gr.getID());
+            }
+            DN grDN = getGroupDN(gr.getID());
+            newMembers.add(grDN.toNormalizedString());
+        }
+        List<String> newAdmins = new ArrayList<String>();
+        for (User<?> member : group.getUserAdmins())
+        {
+            DN memberDN = userPersist.getUserDN(member);
+            newAdmins.add(memberDN.toNormalizedString());
+        }
+        for (Group gr : group.getGroupAdmins())
+        {
+            if (!checkGroupExists(gr.getID()))
+            {
+                throw new GroupNotFoundException(gr.getID());
+            }
+            DN grDN = getGroupDN(gr.getID());
+            newAdmins.add(grDN.toNormalizedString());
+        }
+
+        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
+        ModifyRequest modifyRequest = new ModifyRequest(getAdminGroupDN(group.getID()), adminMods);
+        try
+        {
+            modifyRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl(
+                            "dn:" + getSubjectDN().toNormalizedString()));
+            LdapDAO.checkLdapResult(getConnection().
+                    modify(modifyRequest).getResultCode());
+            
+            // modify the group itself now
+            modifyRequest = new ModifyRequest(getGroupDN(group.getID()), mods);
+
+            modifyRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl(
+                            "dn:" + getSubjectDN().toNormalizedString()));
+            LdapDAO.checkLdapResult(getConnection().
+                    modify(modifyRequest).getResultCode());
+        }
+        catch (LDAPException e1)
+        {
+            LdapDAO.checkLdapResult(e1.getResultCode());
+        }
+        try
+        {
+            if (withActivate)
+            {
+                return new ActivatedGroup(getGroup(group.getID()));
+            }
+            else
+            {
+                return getGroup(group.getID());
+            }
+        }
+        catch (GroupNotFoundException e)
+        {
+            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.
+     */
+    public void deleteGroup(final String groupID)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException
+    {
+        deleteGroup(getGroupDN(groupID), groupID, false);
+        deleteGroup(getAdminGroupDN(groupID), groupID, true);
+    }
+    
+    private void deleteGroup(final DN groupDN, final String groupID, 
+                             final boolean isAdmin)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException
+    {
+        Group group = getGroup(groupDN, groupID, true);
+        List<Modification> modifs = new ArrayList<Modification>();
+        modifs.add(new Modification(ModificationType.ADD, "nsaccountlock", "true"));
+        
+        if (isAdmin)
+        {
+            if (!group.getGroupAdmins().isEmpty() || 
+                !group.getUserAdmins().isEmpty())
+            {
+                modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
+            }
+        }
+        else
+        {
+            if (!group.getGroupMembers().isEmpty() || 
+                !group.getUserMembers().isEmpty())
+            {
+                modifs.add(new Modification(ModificationType.DELETE, "uniquemember"));
+            }
+        }
+
+        ModifyRequest modifyRequest = new ModifyRequest(groupDN, modifs);
+        try
+        {
+            modifyRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl(
+                            "dn:" + getSubjectDN().toNormalizedString()));
+            LDAPResult result = getConnection().modify(modifyRequest);
+            LdapDAO.checkLdapResult(result.getResultCode());
+        }
+        catch (LDAPException e1)
+        {
+            LdapDAO.checkLdapResult(e1.getResultCode());
+        }
+        
+        try
+        {
+            getGroup(group.getID());
+            throw new RuntimeException("BUG: group not deleted " + 
+                                       group.getID());
+        }
+        catch (GroupNotFoundException ignore) {}
+    }
+    
+    /**
+     * Obtain a Collection of Groups that fit the given query.
+     * 
+     * @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.
+     * @throws TransientException  If an temporary, unexpected problem occurred.
+     * @throws UserNotFoundException
+     * @throws GroupNotFoundException
+     */
+    public Collection<Group> getGroups(final T userID, final Role role, 
+                                       final String groupID)
+        throws TransientException, AccessControlException,
+               GroupNotFoundException, UserNotFoundException
+    {
+        User<T> user = new User<T>(userID);
+        DN userDN = null;
+        try
+        {
+            userDN = userPersist.getUserDN(user);
+        }
+        catch (UserNotFoundException e)
+        {
+            // no anonymous searches
+            throw new AccessControlException("Not authorized to search");
+        }
+        
+        Collection<DN> groupDNs = new HashSet<DN>();
+        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));
+        }
+        
+        if (logger.isDebugEnabled())
+        {
+            for (DN dn : groupDNs)
+            {
+                logger.debug("Search adding DN: " + dn);
+            }
+        }
+        
+        Collection<Group> groups = new HashSet<Group>();
+        try
+        {
+            for (DN groupDN : groupDNs)
+            {
+                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)
+                {
+                    final String message = "BUG: group " + groupDN + " not found but " +
+                                           "membership exists (" + userID + ")";
+                    logger.error(message);
+                    //throw new IllegalStateException(message);
+                }
+            }
+        }
+        catch (LDAPException e)
+        {
+            throw new TransientException("Error getting group", e);
+        }
+        return groups;
+    }
+    
+    protected Collection<DN> getOwnerGroups(final User<T> user, 
+                                            final DN userDN,
+                                            final String groupID)
+        throws TransientException, AccessControlException,
+               GroupNotFoundException, UserNotFoundException
+    {
+        Collection<DN> groupDNs = new HashSet<DN>();
+        try
+        {                           
+            Filter filter = Filter.createEqualityFilter("owner", 
+                                                        userDN.toString());
+            if (groupID != null)
+            {
+                getGroup(groupID);
+                filter = Filter.createANDFilter(filter, 
+                                Filter.createEqualityFilter("cn", groupID));
+            }
+            
+            SearchRequest searchRequest =  new SearchRequest(
+                    config.getGroupsDN(), SearchScope.SUB, filter, "entrydn", "nsaccountlock");
+            
+            searchRequest.addControl(
+                    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));
+                }
+                
+            }
+        }
+        catch (LDAPException e1)
+        {
+            LdapDAO.checkLdapResult(e1.getResultCode());
+        }
+        return groupDNs; 
+    }
+    
+    protected Collection<DN> getMemberGroups(final User<T> user, 
+                                             final DN userDN, 
+                                             final String groupID,
+                                             final boolean isAdmin)
+        throws TransientException, AccessControlException,
+               GroupNotFoundException, UserNotFoundException
+    {
+        Collection<DN> groupDNs = new HashSet<DN>();
+        if (groupID != null)
+        {
+            DN groupDN;
+            if (isAdmin)
+            {
+                groupDN = getAdminGroupDN(groupID);
+            }
+            else
+            {
+                groupDN = getGroupDN(groupID);
+            }
+            if (userPersist.isMember(user.getUserID(),
+                                     groupDN.toNormalizedString()))
+            {
+                groupDNs.add(groupDN);
+            }
+        }
+        else
+        {
+            Collection<DN> memberGroupDNs = 
+                    userPersist.getUserGroups(user.getUserID(), isAdmin);
+            groupDNs.addAll(memberGroupDNs);
+        }
+        return groupDNs;
+    }
+    
+    /**
+     * Returns a group based on its LDAP DN. The returned group is bare
+     * (contains only group ID, description, modifytimestamp).
+     * 
+     * @param groupDN
+     * @return
+     * @throws com.unboundid.ldap.sdk.LDAPException
+     * @throws ca.nrc.cadc.ac.GroupNotFoundException
+     */
+    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.getMember(
+                                        new DN(searchResult.getAttributeValue(
+                                                "owner"))));
+        group.description = searchResult.getAttributeValue("description");
+        return group;
+    }
+
+    /**
+     * 
+     * @param groupID
+     * @return 
+     */
+    protected DN getGroupDN(final String groupID) throws TransientException
+    {
+        try
+        {
+            return new DN("cn=" + groupID + "," + config.getGroupsDN());
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+        throw new IllegalArgumentException(groupID + " not a valid group ID");
+    }
+    
+    /**
+     * 
+     * @param groupID
+     * @return 
+     */
+    protected DN getAdminGroupDN(final String groupID) throws TransientException
+    {
+        try
+        {
+            return new DN("cn=" + groupID + "," + config.getAdminGroupsDN());
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+        throw new IllegalArgumentException(groupID + " not a valid group ID");
+    }
+    
+    /**
+     * 
+     * @param owner
+     * @return
+     * @throws UserNotFoundException 
+     */
+    protected boolean isCreatorOwner(final User<? extends Principal> owner)
+        throws UserNotFoundException
+    {
+        try
+        {
+            User<X500Principal> subjectUser = 
+                    userPersist.getMember(getSubjectDN());
+            if (subjectUser.equals(owner))
+            {
+                return true;
+            }
+            return false;
+        }
+        catch (LDAPException e)
+        {
+            throw new RuntimeException(e);
+        }
+    }
+    
+    private boolean checkGroupExists(String groupID) 
+            throws TransientException
+    {
+        for (String groupName : getGroupNames())
+        {
+            if (groupName.equalsIgnoreCase(groupID))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
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
new file mode 100755
index 0000000000000000000000000000000000000000..f59bc1518d82c3522bf0cd104bc3ca8fecfc7ebf
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapGroupPersistence.java
@@ -0,0 +1,252 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
+
+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.server.GroupPersistence;
+import ca.nrc.cadc.net.TransientException;
+
+public class LdapGroupPersistence<T extends Principal>
+    implements GroupPersistence<T>
+{
+    private static final Logger log = 
+            Logger.getLogger(LdapGroupPersistence.class);
+    private final LdapConfig config;
+
+    public LdapGroupPersistence()
+    {
+        config = LdapConfig.getLdapConfig();
+    }
+    
+    public Collection<String> getGroupNames()
+        throws TransientException, AccessControlException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            Collection<String> ret = groupDAO.getGroupNames();
+            return ret;
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+    
+    public Group getGroup(String groupName)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            Group ret = groupDAO.getGroup(groupName);
+            return ret;
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    public Group addGroup(Group group)
+        throws GroupAlreadyExistsException, TransientException, 
+               AccessControlException, UserNotFoundException, 
+               GroupNotFoundException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            Group ret = groupDAO.addGroup(group);
+            return ret;
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    public void deleteGroup(String groupName)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            groupDAO.deleteGroup(groupName);
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    public Group modifyGroup(Group group)
+        throws GroupNotFoundException, TransientException,
+               AccessControlException, UserNotFoundException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            Group ret = groupDAO.modifyGroup(group);
+            return ret;
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+    public Collection<Group> getGroups(T userID, Role role, String groupID)
+        throws UserNotFoundException, GroupNotFoundException,
+               TransientException, AccessControlException
+    {
+        LdapGroupDAO<T> groupDAO = null;
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(config);
+            groupDAO = new LdapGroupDAO<T>(config, userDAO);
+            Collection<Group> ret = groupDAO.getGroups(userID, role, groupID);
+            return ret;
+        }
+        finally
+        {
+            if (groupDAO != null)
+            {
+                groupDAO.close();
+            }
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
new file mode 100755
index 0000000000000000000000000000000000000000..f929385511697284380d6e5f6f4abef2d6457ce9
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAO.java
@@ -0,0 +1,438 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import javax.security.auth.x500.X500Principal;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+import com.unboundid.ldap.sdk.*;
+import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.net.TransientException;
+
+
+public class LdapUserDAO<T extends Principal> extends LdapDAO
+{
+    private static final Logger logger = Logger.getLogger(LdapUserDAO.class);
+
+    // Map of identity type to LDAP attribute
+    private Map<Class<?>, String> userLdapAttrib =
+            new HashMap<Class<?>, String>();
+
+    // User attributes returned to the GMS
+    private static final String LDAP_FNAME = "givenname";
+    private static final String LDAP_LNAME = "sn";
+    //TODO to add the rest
+    private String[] userAttribs = new String[]{LDAP_FNAME, LDAP_LNAME};
+    private String[] memberAttribs = new String[]{LDAP_FNAME, LDAP_LNAME};
+
+    public LdapUserDAO(LdapConfig config)
+    {
+        super(config);
+        this.userLdapAttrib.put(HttpPrincipal.class, "uid");
+        this.userLdapAttrib.put(X500Principal.class, "distinguishedname");
+
+        // add the id attributes to user and member attributes
+        String[] princs = userLdapAttrib.values()
+                .toArray(new String[userLdapAttrib.values().size()]);
+        String[] tmp = new String[userAttribs.length + princs.length];
+        System.arraycopy(princs, 0, tmp, 0, princs.length);
+        System.arraycopy(userAttribs, 0, tmp, princs.length,
+                         userAttribs.length);
+        userAttribs = tmp;
+
+        tmp = new String[memberAttribs.length + princs.length];
+        System.arraycopy(princs, 0, tmp, 0, princs.length);
+        System.arraycopy(memberAttribs, 0, tmp, princs.length,
+                         memberAttribs.length);
+        memberAttribs = tmp;
+    }
+
+
+    /**
+     * Get the user specified by userID.
+     *
+     * @param userID The userID.
+     * @return User instance.
+     * @throws UserNotFoundException  when the user is not found.
+     * @throws TransientException     If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public User<T> getUser(T userID)
+            throws UserNotFoundException, TransientException,
+                   AccessControlException
+    {
+        String searchField = userLdapAttrib.get(userID.getClass());
+        if (searchField == null)
+        {
+            throw new IllegalArgumentException(
+                    "Unsupported principal type " + userID.getClass());
+        }
+
+        searchField =
+                "(&(objectclass=cadcaccount)(" + searchField + "=" + userID
+                        .getName() + "))";
+
+        SearchResultEntry searchResult = null;
+        try
+        {
+            SearchRequest searchRequest = new SearchRequest(config.getUsersDN(),
+                                                            SearchScope.SUB,
+                                                            searchField,
+                                                            userAttribs);
+
+            searchRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
+                                                             getSubjectDN()
+                                                                     .toNormalizedString()));
+
+            searchResult = getConnection().searchForEntry(searchRequest);
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+
+        if (searchResult == null)
+        {
+            String msg = "User not found " + userID.toString();
+            logger.debug(msg);
+            throw new UserNotFoundException(msg);
+        }
+        User<T> user = new User<T>(userID);
+        user.getIdentities().add(
+                new HttpPrincipal(searchResult.getAttributeValue(userLdapAttrib
+                                                                         .get(HttpPrincipal.class))));
+
+        String fname = searchResult.getAttributeValue(LDAP_FNAME);
+        String lname = searchResult.getAttributeValue(LDAP_LNAME);
+        user.details.add(new PersonalDetails(fname, lname));
+        //TODO populate user with the other returned personal or posix attributes
+        return user;
+    }
+
+    /**
+     * Get all groups the user specified by userID belongs to.
+     *
+     * @param userID  The userID.
+     * @param isAdmin
+     * @return Collection of Group instances.
+     * @throws UserNotFoundException  when the user is not found.
+     * @throws TransientException     If an temporary, unexpected problem occurred., e.getMessage(
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public Collection<DN> getUserGroups(final T userID, final boolean isAdmin)
+            throws UserNotFoundException, TransientException,
+                   AccessControlException
+    {
+        Collection<DN> groupDNs = new HashSet<DN>();
+        try
+        {
+            String searchField = userLdapAttrib.get(userID.getClass());
+            if (searchField == null)
+            {
+                throw new IllegalArgumentException(
+                        "Unsupported principal type " + userID.getClass());
+            }
+
+            User<T> user = getUser(userID);
+            Filter filter = Filter.createANDFilter(
+                    Filter.createEqualityFilter(searchField,
+                                                user.getUserID().getName()),
+                    Filter.createPresenceFilter("memberOf"));
+
+            SearchRequest searchRequest =
+                    new SearchRequest(config.getUsersDN(), SearchScope.SUB,
+                                      filter, "memberOf");
+
+            searchRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
+                                                             getSubjectDN()
+                                                                     .toNormalizedString()));
+
+            SearchResultEntry searchResult =
+                    getConnection().searchForEntry(searchRequest);
+
+            DN parentDN;
+            if (isAdmin)
+            {
+                parentDN = new DN(config.getAdminGroupsDN());
+            }
+            else
+            {
+                parentDN = new DN(config.getGroupsDN());
+            }
+
+            if (searchResult != null)
+            {
+                String[] members = searchResult.getAttributeValues("memberOf");
+                if (members != null)
+                {
+                    for (String member : members)
+                    {
+                        DN groupDN = new DN(member);
+                        if (groupDN.isDescendantOf(parentDN, false))
+                        {
+                            groupDNs.add(groupDN);
+                        }
+                    }
+                }
+            }
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+        return groupDNs;
+    }
+
+    /**
+     * Check whether the user is a member of the group.
+     *
+     * @param userID  The userID.
+     * @param groupID The groupID.
+     * @return true or false
+     * @throws UserNotFoundException  If the user is not found.
+     * @throws TransientException     If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public boolean isMember(T userID, String groupID)
+            throws UserNotFoundException, TransientException,
+                   AccessControlException
+    {
+        try
+        {
+            String searchField = userLdapAttrib.get(userID.getClass());
+            if (searchField == null)
+            {
+                throw new IllegalArgumentException(
+                        "Unsupported principal type " + userID.getClass());
+            }
+
+            User<T> user = getUser(userID);
+            Filter filter = Filter.createANDFilter(
+                    Filter.createEqualityFilter(searchField,
+                                                user.getUserID().getName()),
+                    Filter.createEqualityFilter("memberOf", groupID));
+
+            SearchRequest searchRequest =
+                    new SearchRequest(config.getUsersDN(), SearchScope.SUB,
+                                      filter, "cn");
+
+            searchRequest.addControl(
+                    new ProxiedAuthorizationV2RequestControl("dn:" +
+                                                             getSubjectDN()
+                                                                     .toNormalizedString()));
+
+            SearchResultEntry searchResults =
+                    getConnection().searchForEntry(searchRequest);
+
+            return (searchResults != null);
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+        return false;
+    }
+
+//    public boolean isMember(T userID, String groupID)
+//        throws UserNotFoundException, TransientException,
+//               AccessControlException
+//    {
+//        try
+//        {
+//            String searchField = (String) userLdapAttrib.get(userID.getClass());
+//            if (searchField == null)
+//            {
+//                throw new IllegalArgumentException(
+//                        "Unsupported principal type " + userID.getClass());
+//            }
+//
+//            User<T> user = getUser(userID);
+//            DN userDN = getUserDN(user);
+//
+//            CompareRequest compareRequest = 
+//                    new CompareRequest(userDN.toNormalizedString(), 
+//                                      "memberOf", groupID);
+//            
+//            compareRequest.addControl(
+//                    new ProxiedAuthorizationV2RequestControl("dn:" + 
+//                            getSubjectDN().toNormalizedString()));
+//            
+//            CompareResult compareResult = 
+//                    getConnection().compare(compareRequest);
+//            return compareResult.compareMatched();
+//        }
+//        catch (LDAPException e)
+//        {
+//            LdapDAO.checkLdapResult(e.getResultCode());
+//            throw new RuntimeException("Unexpected LDAP exception", e);
+//        }
+//    }
+
+    /**
+     * Returns a member user identified by the X500Principal only. The
+     * returned object has the fields required by the GMS.
+     * Note that this method binds as a proxy user and not as the
+     * subject.
+     *
+     * @param userDN
+     * @return
+     * @throws UserNotFoundException
+     * @throws LDAPException
+     */
+    User<X500Principal> getMember(DN userDN)
+            throws UserNotFoundException, LDAPException
+    {
+        Filter filter =
+                Filter.createEqualityFilter("entrydn",
+                                            userDN.toNormalizedString());
+
+        SearchRequest searchRequest =
+                new SearchRequest(this.config.getUsersDN(), SearchScope.SUB,
+                                  filter, memberAttribs);
+
+        SearchResultEntry searchResult =
+                getConnection().searchForEntry(searchRequest);
+
+        if (searchResult == null)
+        {
+            String msg = "Member not found " + userDN;
+            logger.debug(msg);
+            throw new UserNotFoundException(msg);
+        }
+        User<X500Principal> user = new User<X500Principal>(
+                new X500Principal(searchResult.getAttributeValue(
+                        userLdapAttrib.get(X500Principal.class))));
+        String princ = searchResult.getAttributeValue(
+                userLdapAttrib.get(HttpPrincipal.class));
+        if (princ != null)
+        {
+            user.getIdentities().add(new HttpPrincipal(princ));
+        }
+        String fname = searchResult.getAttributeValue(LDAP_FNAME);
+        String lname = searchResult.getAttributeValue(LDAP_LNAME);
+        user.details.add(new PersonalDetails(fname, lname));
+        return user;
+    }
+
+
+    DN getUserDN(User<? extends Principal> user)
+            throws UserNotFoundException, TransientException
+    {
+        String searchField =
+                userLdapAttrib.get(user.getUserID().getClass());
+        if (searchField == null)
+        {
+            throw new IllegalArgumentException(
+                    "Unsupported principal type " + user.getUserID()
+                            .getClass());
+        }
+
+        searchField = "(" + searchField + "=" +
+                      user.getUserID().getName() + ")";
+
+        SearchResultEntry searchResult = null;
+        try
+        {
+            SearchRequest searchRequest =
+                    new SearchRequest(this.config.getUsersDN(), SearchScope.SUB,
+                                      searchField, "entrydn");
+
+
+            searchResult =
+                    getConnection().searchForEntry(searchRequest);
+
+        }
+        catch (LDAPException e)
+        {
+            LdapDAO.checkLdapResult(e.getResultCode());
+        }
+
+        if (searchResult == null)
+        {
+            String msg = "User not found " + user.getUserID().getName();
+            logger.debug(msg);
+            throw new UserNotFoundException(msg);
+        }
+        return searchResult.getAttributeValueAsDN("entrydn");
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
new file mode 100755
index 0000000000000000000000000000000000000000..8511d254f2ab164ad4428e9ab929f9e782bf852b
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/ldap/LdapUserPersistence.java
@@ -0,0 +1,193 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.net.TransientException;
+import com.unboundid.ldap.sdk.DN;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.Collection;
+import org.apache.log4j.Logger;
+
+public class LdapUserPersistence<T extends Principal>
+    implements UserPersistence<T>
+{
+    private static final Logger logger = Logger.getLogger(LdapUserPersistence.class);
+    private LdapConfig config;
+
+    public LdapUserPersistence()
+    {
+        try
+        {
+            this.config = LdapConfig.getLdapConfig();
+        }
+        catch (RuntimeException e)
+        {
+            logger.error("test/config/LdapConfig.properties file required.", e);
+        }
+    }
+
+    /**
+     * Get the user specified by userID.
+     *
+     * @param userID The userID.
+     *
+     * @return User instance.
+     * 
+     * @throws UserNotFoundException when the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public User<T> getUser(T userID)
+        throws UserNotFoundException, TransientException, AccessControlException
+    {
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(this.config);
+            User<T> ret = userDAO.getUser(userID);
+            return ret;
+        }
+        finally
+        {
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+    
+    /**
+     * Get all groups the user specified by userID belongs to.
+     * 
+     * @param userID The userID.
+     * @param isAdmin return only admin Groups when true, else return non-admin
+     *                Groups.
+     * 
+     * @return Collection of Group DN.
+     * 
+     * @throws UserNotFoundException  when the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public Collection<DN> getUserGroups(T userID, boolean isAdmin)
+        throws UserNotFoundException, TransientException, AccessControlException
+    {
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(this.config);
+            Collection<DN> ret = userDAO.getUserGroups(userID, isAdmin);
+            return ret;
+        }
+        finally
+        {
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+    
+    /**
+     * Check whether the user is a member of the group.
+     *
+     * @param userID The userID.
+     * @param groupID The groupID.
+     *
+     * @return true or false
+     *
+     * @throws UserNotFoundException If the user is not found.
+     * @throws TransientException If an temporary, unexpected problem occurred.
+     * @throws AccessControlException If the operation is not permitted.
+     */
+    public boolean isMember(T userID, String groupID)
+        throws UserNotFoundException, TransientException,
+               AccessControlException
+    {
+        LdapUserDAO<T> userDAO = null;
+        try
+        {
+            userDAO = new LdapUserDAO<T>(this.config);
+            boolean ret = userDAO.isMember(userID, groupID);
+            return ret;
+        }
+        finally
+        {
+            if (userDAO != null)
+            {
+                userDAO.close();
+            }
+        }
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java
new file mode 100755
index 0000000000000000000000000000000000000000..d608fb82b1c65fbd214996b05eef78fc878b1fbd
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ACSearchRunner.java
@@ -0,0 +1,389 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.IOException;
+import java.security.AccessControlContext;
+import java.security.AccessControlException;
+import java.security.AccessController;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.GroupsWriter;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.PluginFactory;
+import ca.nrc.cadc.ac.server.RequestValidator;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.net.TransientException;
+import ca.nrc.cadc.uws.ExecutionPhase;
+import ca.nrc.cadc.uws.Job;
+import ca.nrc.cadc.uws.server.JobRunner;
+import ca.nrc.cadc.uws.server.JobUpdater;
+import ca.nrc.cadc.uws.server.SyncOutput;
+import ca.nrc.cadc.uws.util.JobLogInfo;
+
+public class ACSearchRunner implements JobRunner
+{
+    private static Logger log = Logger.getLogger(ACSearchRunner.class);
+    
+    private JobUpdater jobUpdater;
+    private SyncOutput syncOut;
+    private Job job;
+    private JobLogInfo logInfo;
+
+    @Override
+    public void setJobUpdater(JobUpdater jobUpdater)
+    {
+        this.jobUpdater = jobUpdater;
+    }
+
+    @Override
+    public void setJob(Job job)
+    {
+        this.job = job;
+    }
+
+    @Override
+    public void setSyncOutput(SyncOutput syncOut)
+    {
+        this.syncOut = syncOut;
+    }
+
+    @Override
+    public void run()
+    {
+        AccessControlContext acContext = AccessController.getContext();
+        Subject subject = Subject.getSubject(acContext);
+        
+        log.debug("RUN ACSearchRunner: " + subject);
+        if (log.isDebugEnabled())
+        {
+            Set<Principal> principals = subject.getPrincipals();
+            Iterator<Principal> i = principals.iterator();
+            while (i.hasNext())
+            {
+                Principal next = i.next();
+                log.debug("Principal " +
+                        next.getClass().getSimpleName()
+                        + ": " + next.getName());
+            }
+        }
+        
+        logInfo = new JobLogInfo(job);
+        logInfo.setSubject(subject);
+
+        String startMessage = logInfo.start();
+        log.info(startMessage);
+
+        long t1 = System.currentTimeMillis();
+        search(subject);
+        long t2 = System.currentTimeMillis();
+
+        logInfo.setElapsedTime(t2 - t1);
+
+        String endMessage = logInfo.end();
+        log.info(endMessage);
+    }
+    
+    @SuppressWarnings("unchecked")
+    private void search(Subject subject)
+    {
+        
+        // Note: This search runner is customized to run with
+        // InMemoryJobPersistence, and synchronous POST requests are
+        // dealt with immediately, rather than returning results via
+        // a redirect.
+        // Jobs in this runner are never updated after execution begins
+        // in case the in-memory job has gone away.  Error reporting
+        // is done directly through the response on both POST and GET
+        
+        try
+        {
+            ExecutionPhase ep = 
+                jobUpdater.setPhase(job.getID(), ExecutionPhase.QUEUED, 
+                                    ExecutionPhase.EXECUTING, new Date());
+            if ( !ExecutionPhase.EXECUTING.equals(ep) )
+            {
+                throw new IllegalStateException("QUEUED -> EXECUTING [FAILED]");
+            }
+            log.debug(job.getID() + ": QUEUED -> EXECUTING [OK]");
+
+            RequestValidator rv = new RequestValidator();
+            rv.validate(job.getParameterList());
+            
+            // only allow users to search themselves...
+            Principal userBeingSearched = rv.getPrincipal();
+            
+            boolean idMatch = false;
+            if (userBeingSearched instanceof X500Principal)
+            {
+                Set<X500Principal> x500Principals = subject.getPrincipals(X500Principal.class);
+                Iterator<X500Principal> i = x500Principals.iterator();
+                while (i.hasNext())
+                {
+                    X500Principal next = i.next();
+                    log.debug(String.format("Comparing x500: [%s][%s]",
+                            next.getName(), userBeingSearched.getName()));
+                    if (AuthenticationUtil.equals(next, userBeingSearched))
+                        idMatch = true;
+                }
+            }
+            else if (userBeingSearched instanceof HttpPrincipal)
+            {
+                Set<HttpPrincipal> httpPrincipals = subject.getPrincipals(HttpPrincipal.class);
+                Iterator<HttpPrincipal> i = httpPrincipals.iterator();
+                while (i.hasNext())
+                {
+                    HttpPrincipal next = i.next();
+                    log.debug(String.format("Comparing http: [%s][%s]",
+                            next.getName(), userBeingSearched.getName()));
+                    if (next.equals(userBeingSearched))
+                        idMatch = true;
+                }
+            }
+            if (!idMatch)
+                throw new AccessControlException("Can only search oneself.");
+
+            PluginFactory factory = new PluginFactory();
+            GroupPersistence dao = factory.getGroupPersistence();
+            Collection<Group> groups = 
+                dao.getGroups(rv.getPrincipal(), rv.getRole(), rv.getGroupID());
+            syncOut.setResponseCode(HttpServletResponse.SC_OK);
+            GroupsWriter.write(groups, syncOut.getOutputStream());
+            
+            // Mark the Job as completed.
+//            jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING, 
+//                                ExecutionPhase.COMPLETED, new Date());
+        }
+        catch (TransientException t)
+        {
+            logInfo.setSuccess(false);
+            logInfo.setMessage(t.getMessage());
+            log.error("FAIL", t);
+            
+            syncOut.setResponseCode(503);
+            syncOut.setHeader("Content-Type", "text/plan");
+            try
+            {
+                syncOut.getOutputStream().write(t.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+            
+//            ErrorSummary errorSummary =
+//                new ErrorSummary(t.getMessage(), ErrorType.FATAL);
+//            try
+//            {
+//                jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+//                                    ExecutionPhase.ERROR, errorSummary, 
+//                                    new Date());
+//            }
+//            catch(Throwable oops)
+//            {
+//                log.debug("failed to set final error status after " + t, oops);
+//            }
+        }
+        catch (UserNotFoundException t)
+        {
+            logInfo.setSuccess(false);
+            logInfo.setMessage(t.getMessage());
+            log.debug("FAIL", t);
+            
+            syncOut.setResponseCode(404);
+            syncOut.setHeader("Content-Type", "text/plan");
+            try
+            {
+                syncOut.getOutputStream().write(t.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+            
+//            ErrorSummary errorSummary =
+//                new ErrorSummary(t.getMessage(), ErrorType.FATAL);
+//            try
+//            {
+//                jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+//                                    ExecutionPhase.ERROR, errorSummary,
+//                                    new Date());
+//            }
+//            catch(Throwable oops)
+//            {
+//                log.debug("failed to set final error status after " + t, oops);
+//            }
+        }
+        catch (GroupNotFoundException t)
+        {
+            logInfo.setSuccess(false);
+            logInfo.setMessage(t.getMessage());
+            log.debug("FAIL", t);
+            
+            syncOut.setResponseCode(404);
+            syncOut.setHeader("Content-Type", "text/plan");
+            try
+            {
+                syncOut.getOutputStream().write(t.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+            
+//            ErrorSummary errorSummary =
+//                new ErrorSummary(t.getMessage(), ErrorType.FATAL);
+//            try
+//            {
+//                jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+//                                    ExecutionPhase.ERROR, errorSummary,
+//                                    new Date());
+//            }
+//            catch(Throwable oops)
+//            {
+//                log.debug("failed to set final error status after " + t, oops);
+//            }
+        }
+        catch (AccessControlException t)
+        {
+            logInfo.setSuccess(false);
+            logInfo.setMessage(t.getMessage());
+            log.debug("FAIL", t);
+            
+            syncOut.setResponseCode(403);
+            syncOut.setHeader("Content-Type", "text/plan");
+            try
+            {
+                syncOut.getOutputStream().write(t.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+            
+//            ErrorSummary errorSummary =
+//                new ErrorSummary(t.getMessage(), ErrorType.FATAL);
+//            try
+//            {
+//                jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+//                                    ExecutionPhase.ERROR, errorSummary,
+//                                    new Date());
+//            }
+//            catch(Throwable oops)
+//            {
+//                log.debug("failed to set final error status after " + t, oops);
+//            }
+        }
+        catch (Throwable t)
+        {
+            logInfo.setSuccess(false);
+            logInfo.setMessage(t.getMessage());
+            log.error("FAIL", t);
+            
+            syncOut.setResponseCode(500);
+            syncOut.setHeader("Content-Type", "text/plan");
+            try
+            {
+                syncOut.getOutputStream().write(t.getMessage().getBytes());
+            }
+            catch (IOException e)
+            {
+                log.warn("Could not write response to output stream", e);
+            }
+            
+//            ErrorSummary errorSummary =
+//                new ErrorSummary(t.getMessage(), ErrorType.FATAL);
+//            try
+//            {
+//                jobUpdater.setPhase(job.getID(), ExecutionPhase.EXECUTING,
+//                                    ExecutionPhase.ERROR, errorSummary,
+//                                    new Date());
+//            }
+//            catch(Throwable oops)
+//            {
+//                log.debug("failed to set final error status after " + t, oops);
+//            }
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..9577061cca259dcc76f5e4dae527685aafc7164d
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddGroupMemberAction.java
@@ -0,0 +1,109 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class AddGroupMemberAction extends GroupsAction
+{
+    private final String groupName;
+    private final String groupMemberName;
+
+    AddGroupMemberAction(GroupLogInfo logInfo, String groupName,
+                         String groupMemberName)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+        this.groupMemberName = groupMemberName;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group group = groupPersistence.getGroup(this.groupName);
+        Group toAdd = groupPersistence.getGroup(this.groupMemberName);
+        if (!group.getGroupMembers().add(toAdd))
+        {
+            throw new GroupAlreadyExistsException(this.groupMemberName);
+        }
+        groupPersistence.modifyGroup(group);
+
+        List<String> addedMembers = new ArrayList<String>();
+        addedMembers.add(toAdd.getID());
+        logGroupInfo(group.getID(), null, addedMembers);
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..d8a84b2650cf2b3d4cd191b5fbf7f4a54014235c
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/AddUserMemberAction.java
@@ -0,0 +1,118 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class AddUserMemberAction extends GroupsAction
+{
+    private final String groupName;
+    private final String userID;
+    private final String userIDType;
+
+    AddUserMemberAction(GroupLogInfo logInfo, String groupName, String userID,
+                        String userIDType)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+        this.userID = userID;
+        this.userIDType = userIDType;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        UserPersistence userPersistence = getUserPersistence();
+        Group group = groupPersistence.getGroup(this.groupName);
+        Principal userPrincipal = AuthenticationUtil.createPrincipal(this.userID, this.userIDType);
+        User toAdd = userPersistence.getUser(userPrincipal);
+        if (!group.getUserMembers().add(toAdd))
+        {
+            throw new MemberAlreadyExistsException();
+        }
+        groupPersistence.modifyGroup(group);
+
+        List<String> addedMembers = new ArrayList<String>();
+        addedMembers.add(toAdd.getUserID().getName());
+        logGroupInfo(group.getID(), null, addedMembers);
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..32661345fc85f1042784a01421c27da98d4b9c48
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/CreateGroupAction.java
@@ -0,0 +1,117 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupReader;
+import ca.nrc.cadc.ac.GroupWriter;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+
+public class CreateGroupAction extends GroupsAction
+{
+    private final InputStream inputStream;
+
+    CreateGroupAction(GroupLogInfo logInfo, InputStream inputStream)
+    {
+        super(logInfo);
+        this.inputStream = inputStream;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group group = GroupReader.read(this.inputStream);
+        Group newGroup = groupPersistence.addGroup(group);
+        this.response.setContentType("application/xml");
+        GroupWriter.write(newGroup, this.response.getOutputStream());
+
+        List<String> addedMembers = null;
+        if ((newGroup.getUserMembers().size() > 0) || (newGroup.getGroupMembers().size() > 0))
+        {
+            addedMembers = new ArrayList<String>();
+            for (Group gr : newGroup.getGroupMembers())
+            {
+                addedMembers.add(gr.getID());
+            }
+            for (User usr : newGroup.getUserMembers())
+            {
+                addedMembers.add(usr.getUserID().getName());
+            }
+        }
+        logGroupInfo(newGroup.getID(), null, addedMembers);
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..8f619fa381cc12469cf9ff0b196c280364d22571
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/DeleteGroupAction.java
@@ -0,0 +1,108 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.util.ArrayList;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+
+public class DeleteGroupAction extends GroupsAction
+{
+    private final String groupName;
+
+    DeleteGroupAction(GroupLogInfo logInfo, String groupName)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group deletedGroup = groupPersistence.getGroup(this.groupName);
+        groupPersistence.deleteGroup(this.groupName);
+        if ((deletedGroup.getUserMembers().size() > 0) || (deletedGroup.getGroupMembers().size() > 0))
+        {
+            this.logInfo.deletedMembers = new ArrayList<String>();
+            for (Group gr : deletedGroup.getGroupMembers())
+            {
+                this.logInfo.deletedMembers.add(gr.getID());
+            }
+            for (User usr : deletedGroup.getUserMembers())
+            {
+                this.logInfo.deletedMembers.add(usr.getUserID().getName());
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..e72003567a83a2723ee6ce15376d5a5beee22027
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupAction.java
@@ -0,0 +1,94 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupWriter;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+
+public class GetGroupAction extends GroupsAction
+{
+    private final String groupName;
+
+    GetGroupAction(GroupLogInfo logInfo, String groupName)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group group = groupPersistence.getGroup(this.groupName);
+        this.response.setContentType("application/xml");
+        GroupWriter.write(group, this.response.getOutputStream());
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..a29e5fcf29a37ebf66ded5fd96cff4edbe77f26c
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GetGroupNamesAction.java
@@ -0,0 +1,111 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.Writer;
+import java.util.Collection;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.server.GroupPersistence;
+
+public class GetGroupNamesAction extends GroupsAction
+{
+    
+    private static final Logger log = Logger.getLogger(GetGroupNamesAction.class);
+
+    GetGroupNamesAction(GroupLogInfo logInfo)
+    {
+        super(logInfo);
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Collection<String> groups = groupPersistence.getGroupNames();
+        log.debug("Found " + groups.size() + " group names");
+        response.setContentType("text/plain");
+        log.debug("Set content-type to text/plain");
+        Writer writer = response.getWriter();
+        boolean start = true;
+        for (final String group : groups)
+        {
+            if (!start)
+            {
+                writer.write("\r\n");
+            }
+            writer.write(group);
+            start = false;
+        }
+        
+        return null;
+    }
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java
new file mode 100755
index 0000000000000000000000000000000000000000..512610114aa61aa3cc24d30a2fd62c3922f93e7c
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupLogInfo.java
@@ -0,0 +1,86 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.log.ServletLogInfo;
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+
+public class GroupLogInfo extends ServletLogInfo
+{
+    public String groupID;
+    public List<String> addedMembers;
+    public List<String> deletedMembers;
+
+    public GroupLogInfo(HttpServletRequest request)
+    {
+        super(request);
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..02f64926a9982e7ae88b89bed6c054087cb473a9
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsAction.java
@@ -0,0 +1,249 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.IOException;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.List;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.MemberNotFoundException;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.PluginFactory;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.net.TransientException;
+
+public abstract class GroupsAction
+    implements PrivilegedExceptionAction<Object>
+{
+    private static final Logger log = Logger.getLogger(GroupsAction.class);
+    protected GroupLogInfo logInfo;
+    protected HttpServletResponse response;
+
+    GroupsAction(GroupLogInfo logInfo)
+    {
+        this.logInfo = logInfo;
+    }
+
+    public void doAction(Subject subject, HttpServletResponse response)
+        throws IOException
+    {
+        try
+        {
+            try
+            {
+                this.response = response;
+
+                if (subject == null)
+                {
+                    run();
+                }
+                else
+                {
+                    Subject.doAs(subject, this);
+                }
+            }
+            catch (PrivilegedActionException e)
+            {
+                Throwable cause = e.getCause();
+                if (cause != null)
+                {
+                    throw cause;
+                }
+                throw e;
+            }
+        }
+        catch (AccessControlException e)
+        {
+            log.debug(e);
+            String message = "Permission Denied";
+            this.logInfo.setMessage(message);
+            sendError(403, message);
+        }
+        catch (IllegalArgumentException e)
+        {
+            log.debug(e);
+            String message = e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(400, message);
+        }
+        catch (MemberNotFoundException e)
+        {
+            log.debug(e);
+            String message = "Member not found: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(404, message);
+        }
+        catch (GroupNotFoundException e)
+        {
+            log.debug(e);
+            String message = "Group not found: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(404, message);
+        }
+        catch (UserNotFoundException e)
+        {
+            log.debug(e);
+            String message = "User not found: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(404, message);
+        }
+        catch (MemberAlreadyExistsException e)
+        {
+            log.debug(e);
+            String message = "Member already exists: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(409, message);
+        }
+        catch (GroupAlreadyExistsException e)
+        {
+            log.debug(e);
+            String message = "Group already exists: " + e.getMessage();
+            this.logInfo.setMessage(message);
+            sendError(409, message);
+        }
+        catch (UnsupportedOperationException e)
+        {
+            log.debug(e);
+            this.logInfo.setMessage("Not yet implemented.");
+            sendError(501);
+        }
+        catch (TransientException e)
+        {
+            String message = "Internal Transient Error: " + e.getMessage();
+            this.logInfo.setSuccess(false);
+            this.logInfo.setMessage(message);
+            log.error(message, e);
+            sendError(503, message);
+        }
+        catch (Throwable t)
+        {
+            String message = "Internal Error: " + t.getMessage();
+            this.logInfo.setSuccess(false);
+            this.logInfo.setMessage(message);
+            log.error(message, t);
+            sendError(500, message);
+        }
+    }
+
+    private void sendError(int responseCode)
+        throws IOException
+    {
+        sendError(responseCode, null);
+    }
+
+    private void sendError(int responseCode, String message)
+        throws IOException
+    {
+        if (!this.response.isCommitted())
+        {
+            this.response.setContentType("text/plain");
+            if (message != null)
+            {
+                this.response.getWriter().write(message);
+            }
+            this.response.setStatus(responseCode);
+        }
+        else
+        {
+            log.warn("Could not send error " + responseCode + " (" + message + ") because the response is already committed.");
+        }
+    }
+
+    <T extends Principal> GroupPersistence<T> getGroupPersistence()
+    {
+        PluginFactory pluginFactory = new PluginFactory();
+        return pluginFactory.getGroupPersistence();
+    }
+
+    <T extends Principal> UserPersistence<T> getUserPersistence()
+    {
+        PluginFactory pluginFactory = new PluginFactory();
+        return pluginFactory.getUserPersistence();
+    }
+
+    protected void logGroupInfo(String groupID, List<String> deletedMembers, List<String> addedMembers)
+    {
+        this.logInfo.groupID = groupID;
+        this.logInfo.addedMembers = addedMembers;
+        this.logInfo.deletedMembers = deletedMembers;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java
new file mode 100755
index 0000000000000000000000000000000000000000..83ecc9d1f7a0caae37d88e1b53d7c7183e1079e5
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsActionFactory.java
@@ -0,0 +1,194 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLDecoder;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.util.StringUtil;
+
+public class GroupsActionFactory
+{
+    private static final Logger log = Logger.getLogger(GroupsActionFactory.class);
+
+    static GroupsAction getGroupsAction(HttpServletRequest request, GroupLogInfo logInfo)
+        throws IOException
+    {
+        GroupsAction action = null;
+        String method = request.getMethod();
+        String path = request.getPathInfo();
+        log.debug("method: " + method);
+        log.debug("path: " + path);
+
+        if (path == null)
+        {
+            path = "";
+        }
+
+        if (path.startsWith("/"))
+        {
+            path = path.substring(1);
+        }
+
+        if (path.endsWith("/"))
+        {
+            path = path.substring(0, path.length() - 1);
+        }
+
+        String[] segments = new String[0];
+        if (StringUtil.hasText(path))
+        {
+            segments = path.split("/");
+        }
+
+        if (segments.length == 0)
+        {
+            if (method.equals("GET"))
+            {
+                action = new GetGroupNamesAction(logInfo);
+            }
+            else if (method.equals("PUT"))
+            {
+                action = new CreateGroupAction(logInfo, request.getInputStream());
+            }
+
+        }
+        else if (segments.length == 1)
+        {
+            String groupName = segments[0];
+            if (method.equals("GET"))
+            {
+                action = new GetGroupAction(logInfo, groupName);
+            }
+            else if (method.equals("DELETE"))
+            {
+                action = new DeleteGroupAction(logInfo, groupName);
+            }
+            else if (method.equals("POST"))
+            {
+                final URL requestURL =
+                        new URL(request.getRequestURL().toString());
+                final String redirectURI = requestURL.getProtocol() + "://"
+                                           + requestURL.getHost() + ":"
+                                           + requestURL.getPort()
+                                           + request.getContextPath()
+                                           + request.getServletPath()
+                                           + "/" + path;
+                action = new ModifyGroupAction(logInfo, groupName, redirectURI,
+                                               request.getInputStream());
+            }
+        }
+        else if (segments.length == 3)
+        {
+            String groupName = segments[0];
+            String memberCategory = segments[1];
+            if (method.equals("PUT"))
+            {
+                if (memberCategory.equals("groupMembers"))
+                {
+                    String groupMemberName = segments[2];
+                    action = new AddGroupMemberAction(logInfo, groupName, groupMemberName);
+                }
+                else if (memberCategory.equals("userMembers"))
+                {
+                    String userMemberID = URLDecoder.decode(segments[2], "UTF-8");
+                    String userMemberIDType = request.getParameter("idType");
+                    action = new AddUserMemberAction(logInfo, groupName, userMemberID, userMemberIDType);
+                }
+            }
+            else if (method.equals("DELETE"))
+            {
+                if (memberCategory.equals("groupMembers"))
+                {
+                    String groupMemberName = segments[2];
+                    action = new RemoveGroupMemberAction(logInfo, groupName, groupMemberName);
+                }
+                else if (memberCategory.equals("userMembers"))
+                {
+                    String memberUserID = URLDecoder.decode(segments[2], "UTF-8");
+                    String memberUserIDType = request.getParameter("idType");
+                    action = new RemoveUserMemberAction(logInfo, groupName, memberUserID, memberUserIDType);
+                }
+            }
+        }
+
+        if (action != null)
+        {
+            log.debug("Returning action: " + action.getClass());
+            return action;
+        }
+        throw new IllegalArgumentException("Bad groups request: " + method + " on " + path);
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java
new file mode 100755
index 0000000000000000000000000000000000000000..dd62ed5cdb871629bb86aa975b1a24e7b583e492
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/GroupsServlet.java
@@ -0,0 +1,161 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.IOException;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+import ca.nrc.cadc.auth.AuthenticationUtil;
+
+public class GroupsServlet extends HttpServlet
+{
+    private static final Logger log = Logger.getLogger(GroupsServlet.class);
+
+    /**
+     * Create a GroupAction and run the action safely.
+     */
+    private void doAction(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        long start = System.currentTimeMillis();
+        GroupLogInfo logInfo = new GroupLogInfo(request);
+        try
+        {
+            log.info(logInfo.start());
+            Subject subject = AuthenticationUtil.getSubject(request);
+            logInfo.setSubject(subject);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, logInfo);
+            action.doAction(subject, response);
+        }
+        catch (IllegalArgumentException e)
+        {
+            log.debug(e.getMessage(), e);
+            logInfo.setMessage(e.getMessage());
+            logInfo.setSuccess(false);
+            response.getWriter().write(e.getMessage());
+            response.setStatus(400);
+        }
+        catch (Throwable t)
+        {
+            String message = "Internal Server Error: " + t.getMessage();
+            log.error(message, t);
+            logInfo.setSuccess(false);
+            logInfo.setMessage(message);
+            response.getWriter().write(message);
+            response.setStatus(500);
+        }
+        finally
+        {
+            logInfo.setElapsedTime(System.currentTimeMillis() - start);
+            log.info(logInfo.end());
+        }
+    }
+
+    @Override
+    public void doGet(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        doAction(request, response);
+    }
+
+    @Override
+    public void doPost(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        doAction(request, response);
+    }
+
+    @Override
+    public void doDelete(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        doAction(request, response);
+    }
+
+    @Override
+    public void doPut(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        doAction(request, response);
+    }
+
+    @Override
+    public void doHead(HttpServletRequest request, HttpServletResponse response)
+        throws IOException
+    {
+        doAction(request, response);
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..c7a03ca5688c0d2c697ff24296428310767edc29
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/ModifyGroupAction.java
@@ -0,0 +1,142 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupReader;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+
+public class ModifyGroupAction extends GroupsAction
+{
+    private final String groupName;
+    private final String request;
+    private final InputStream inputStream;
+
+    ModifyGroupAction(GroupLogInfo logInfo, String groupName,
+                      final String request, InputStream inputStream)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+        this.request = request;
+        this.inputStream = inputStream;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group group = GroupReader.read(this.inputStream);
+        Group oldGroup = groupPersistence.getGroup(this.groupName);
+        groupPersistence.modifyGroup(group);
+
+        List<String> addedMembers = new ArrayList<String>();
+        for (User member : group.getUserMembers())
+        {
+            if (!oldGroup.getUserMembers().remove(member))
+            {
+                addedMembers.add(member.getUserID().getName());
+            }
+        }
+        for (Group gr : group.getGroupMembers())
+        {
+            if (!oldGroup.getGroupMembers().remove(gr))
+            {
+                addedMembers.add(gr.getID());
+            }
+        }
+        if (addedMembers.isEmpty())
+        {
+            addedMembers = null;
+        }
+        List<String> deletedMembers = new ArrayList<String>();
+        for (User member : oldGroup.getUserMembers())
+        {
+            deletedMembers.add(member.getUserID().getName());
+        }
+        for (Group gr : oldGroup.getGroupMembers())
+        {
+            deletedMembers.add(gr.getID());
+        }
+        if (deletedMembers.isEmpty())
+        {
+            deletedMembers = null;
+        }
+        logGroupInfo(group.getID(), deletedMembers, addedMembers);
+
+        this.response.sendRedirect(request);
+
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..39e148916a204e9369340fe50a5b0eabc22c69bf
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberAction.java
@@ -0,0 +1,108 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class RemoveGroupMemberAction extends GroupsAction
+{
+    private final String groupName;
+    private final String groupMemberName;
+
+    RemoveGroupMemberAction(GroupLogInfo logInfo, String groupName, String groupMemberName)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+        this.groupMemberName = groupMemberName;
+    }
+
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        Group group = groupPersistence.getGroup(this.groupName);
+        Group toRemove = groupPersistence.getGroup(this.groupMemberName);
+        if (!group.getGroupMembers().remove(toRemove))
+        {
+            throw new GroupNotFoundException(this.groupMemberName);
+        }
+        groupPersistence.modifyGroup(group);
+
+        List<String> deletedMembers = new ArrayList<String>();
+        deletedMembers.add(toRemove.getID());
+        logGroupInfo(group.getID(), deletedMembers, null);
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..c9612f663c5009d88c9d00d17fa82974a266a6bb
--- /dev/null
+++ b/projects/cadcAccessControl-Server/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberAction.java
@@ -0,0 +1,116 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.MemberNotFoundException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RemoveUserMemberAction extends GroupsAction
+{
+    private final String groupName;
+    private final String userID;
+    private final String userIDType;
+
+    RemoveUserMemberAction(GroupLogInfo logInfo, String groupName, String userID, String userIDType)
+    {
+        super(logInfo);
+        this.groupName = groupName;
+        this.userID = userID;
+        this.userIDType = userIDType;
+    }
+
+    @SuppressWarnings("unchecked")
+    public Object run()
+        throws Exception
+    {
+        GroupPersistence groupPersistence = getGroupPersistence();
+        UserPersistence userPersistence = getUserPersistence();
+        Group group = groupPersistence.getGroup(this.groupName);
+        Principal userPrincipal = AuthenticationUtil.createPrincipal(this.userID, this.userIDType);
+        User toRemove = userPersistence.getUser(userPrincipal);
+        if (!group.getUserMembers().remove(toRemove))
+        {
+            throw new MemberNotFoundException();
+        }
+        groupPersistence.modifyGroup(group);
+
+        List<String> deletedMembers = new ArrayList<String>();
+        deletedMembers.add(toRemove.getUserID().getName());
+        logGroupInfo(group.getID(), deletedMembers, null);
+        return null;
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..94760917ca297b709b57a06d0700769e95dd9d05
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/RequestValidatorTest.java
@@ -0,0 +1,200 @@
+/*
+ ************************************************************************
+ *******************  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.server;
+
+import ca.nrc.cadc.ac.Role;
+import ca.nrc.cadc.ac.server.web.AddUserMemberActionTest;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.util.Log4jInit;
+import ca.nrc.cadc.uws.Parameter;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class RequestValidatorTest
+{
+    private final static Logger log = Logger.getLogger(AddUserMemberActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    /**
+     * Test of validate method, of class RequestValidator.
+     */
+    @Test
+    public void testValidate()
+    {
+        try
+        {   
+            RequestValidator rv = new RequestValidator();
+            
+            try
+            {
+                rv.validate(null);
+                fail("null parameter list should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            List<Parameter> paramList = new ArrayList<Parameter>();
+            try
+            {
+                rv.validate(paramList);
+                fail("empty parameter list should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.add(new Parameter("IDTYPE", "idtype"));
+            paramList.add(new Parameter("ROLE", "role"));
+            try
+            {
+                rv.validate(paramList);
+                fail("missing ID parameter should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.clear();
+            paramList.add(new Parameter("ID", "foo"));
+            paramList.add(new Parameter("ROLE", "role"));
+            try
+            {
+                rv.validate(paramList);
+                fail("missing IDTYPE parameter should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.clear();
+            paramList.add(new Parameter("ID", "foo"));
+            paramList.add(new Parameter("IDTYPE", "idtype"));
+            try
+            {
+                rv.validate(paramList);
+                fail("missing ROLE parameter should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.clear();
+            paramList.add(new Parameter("ID", "foo"));
+            paramList.add(new Parameter("IDTYPE", AuthenticationUtil.AUTH_TYPE_HTTP));
+            paramList.add(new Parameter("ROLE", "foo"));
+            try
+            {
+                rv.validate(paramList);
+                fail("invalid ROLE parameter should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.clear();
+            paramList.add(new Parameter("ID", "foo"));
+            paramList.add(new Parameter("IDTYPE", AuthenticationUtil.AUTH_TYPE_HTTP));
+            paramList.add(new Parameter("ROLE", "foo"));
+            paramList.add(new Parameter("GROUPID", ""));
+            try
+            {
+                rv.validate(paramList);
+                fail("empty GROUPID parameter value should throw IllegalArgumentException");
+            }
+            catch (IllegalArgumentException ignore) {}
+            
+            paramList.clear();
+            paramList.add(new Parameter("ID", "foo"));
+            paramList.add(new Parameter("IDTYPE", AuthenticationUtil.AUTH_TYPE_HTTP));
+            paramList.add(new Parameter("ROLE", Role.MEMBER.getValue()));
+            rv.validate(paramList);
+            
+            assertNotNull(rv.getPrincipal());
+            assertNotNull(rv.getRole());
+            assertNull(rv.getGroupID());
+            
+            paramList.add(new Parameter("GROUPID", "bar"));
+            rv.validate(paramList);
+            
+            assertNotNull(rv.getPrincipal());
+            assertNotNull(rv.getRole());
+            assertNotNull(rv.getGroupID());
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/AbstractLdapDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/AbstractLdapDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5801981ac380b8f2314d4b321952cd86d15789ea
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/AbstractLdapDAOTest.java
@@ -0,0 +1,82 @@
+/**
+ ************************************************************************
+ *******************  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/>.
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.server.ldap;
+
+/**
+ * Created by jburke on 2014-11-03.
+ */
+public class AbstractLdapDAOTest
+{
+    static final String CONFIG = LdapConfig.class.getSimpleName() + ".test.properties";
+
+    static protected LdapConfig getLdapConfig()
+    {
+        return LdapConfig.getLdapConfig(CONFIG);
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ecd65e9483ab73d8813a600fc6833ca38dbfb673
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTest.java
@@ -0,0 +1,177 @@
+/**
+ ************************************************************************
+ *******************  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/>.
+ *
+ ************************************************************************
+ */
+
+
+package ca.nrc.cadc.ac.server.ldap;
+
+import java.security.PrivilegedExceptionAction;
+
+import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+
+import com.unboundid.ldap.sdk.LDAPConnection;
+
+import org.junit.Test;
+import org.junit.BeforeClass;
+import static org.junit.Assert.*;
+
+
+public class LdapDAOTest extends AbstractLdapDAOTest
+{
+    static LdapConfig config;
+    
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception
+    {
+        // get the configuration of the development server from and config files...
+        config = getLdapConfig();
+    }
+    @Test
+    public void testLdapBindConnection() throws Exception
+    {
+        //TODO use a test user to test with. To be done when addUser available.
+        //LdapUserDAO<X500Principal> userDAO = new LdapUserDAO<X500Principal>();
+        final X500Principal subjPrincipal = new X500Principal(
+                "cn=cadcdaotest1,ou=cadc,o=hia,c=ca");
+
+        // User authenticated with HttpPrincipal
+        HttpPrincipal httpPrincipal = new HttpPrincipal("CadcDaoTest1");
+        Subject subject = new Subject();
+
+        subject.getPrincipals().add(httpPrincipal);
+        
+        final LdapDAOTestImpl ldapDao = new LdapDAOTestImpl(config);
+
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    testConnection(ldapDao.getConnection());
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+               
+
+        subject = new Subject();
+        subject.getPrincipals().add(subjPrincipal);
+        
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    testConnection(ldapDao.getConnection());
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+        
+        
+        NumericPrincipal numPrincipal = new NumericPrincipal(1866);       
+        subject.getPrincipals().add(numPrincipal);
+
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+
+                    testConnection(ldapDao.getConnection());
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+
+    }
+
+    private void testConnection(final LDAPConnection ldapCon)
+    {
+        assertTrue("Not connected but should be.", ldapCon.isConnected());
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d3be479f44fc01e2eaaab7805e645a1dc9f573a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapDAOTestImpl.java
@@ -0,0 +1,90 @@
+/**
+ ************************************************************************
+ *******************  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/>.
+ *
+ ************************************************************************
+ */
+
+
+package ca.nrc.cadc.ac.server.ldap;
+
+import java.security.AccessControlException;
+
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+
+
+public class LdapDAOTestImpl extends LdapDAO
+{
+    public LdapDAOTestImpl(LdapConfig config)
+    {
+        super(config);
+    }
+
+    @Override
+    public LDAPConnection getConnection() throws LDAPException,
+                                                 AccessControlException
+    {
+        return super.getConnection();
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..7af54bc0ae0a9ec3c1e3621a31038538440a7ae9
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapGroupDAOTest.java
@@ -0,0 +1,937 @@
+/**
+ ************************************************************************
+ *******************  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/>.
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.server.ldap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.security.AccessControlException;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import ca.nrc.cadc.ac.ActivatedGroup;
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.GroupProperty;
+import ca.nrc.cadc.ac.Role;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.util.Log4jInit;
+import static org.junit.Assert.assertNotNull;
+
+public class LdapGroupDAOTest extends AbstractLdapDAOTest
+{
+    private static final Logger log = Logger.getLogger(LdapGroupDAOTest.class);
+    
+    static String daoTestDN1 = "cn=cadcdaotest1,ou=cadc,o=hia,c=ca";
+    static String daoTestDN2 = "cn=cadcdaotest2,ou=cadc,o=hia,c=ca";
+    static String daoTestDN3 = "cn=cadcdaotest3,ou=cadc,o=hia,c=ca";
+    static String unknownDN = "cn=foo,ou=cadc,o=hia,c=ca";
+    
+    static X500Principal daoTestPrincipal1;
+    static X500Principal daoTestPrincipal2;
+    static X500Principal daoTestPrincipal3;
+    static X500Principal unknownPrincipal;
+
+    static User<X500Principal> daoTestUser1;
+    static User<X500Principal> daoTestUser2;
+    static User<X500Principal> daoTestUser3;
+    static User<X500Principal> unknownUser;
+
+    static Subject daoTestUser1Subject;
+    static Subject daoTestUser2Subject;
+    static Subject anonSubject;
+
+    static LdapConfig config;
+    
+    @BeforeClass
+    public static void setUpBeforeClass()
+        throws Exception
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+        
+        // get the configuration of the development server from and config files...
+        config = getLdapConfig();
+        
+        daoTestPrincipal1 = new X500Principal(daoTestDN1);
+        daoTestPrincipal2 = new X500Principal(daoTestDN2);
+        daoTestPrincipal3 = new X500Principal(daoTestDN3);
+        unknownPrincipal = new X500Principal(unknownDN);
+
+        daoTestUser1 = new User<X500Principal>(daoTestPrincipal1);
+        daoTestUser2 = new User<X500Principal>(daoTestPrincipal2);
+        daoTestUser3 = new User<X500Principal>(daoTestPrincipal3);
+        unknownUser = new User<X500Principal>(unknownPrincipal);
+        
+        daoTestUser1Subject = new Subject();
+        daoTestUser1Subject.getPrincipals().add(daoTestUser1.getUserID());
+        
+        daoTestUser2Subject = new Subject();
+        daoTestUser2Subject.getPrincipals().add(daoTestUser2.getUserID());
+        
+        anonSubject = new Subject();
+        anonSubject.getPrincipals().add(unknownUser.getUserID());
+    }
+
+    LdapGroupDAO<X500Principal> getGroupDAO()
+    {
+        return new LdapGroupDAO<X500Principal>(config,
+                new LdapUserDAO<X500Principal>(config));
+    }
+    
+    String getGroupID()
+    {
+        return "CadcDaoTestGroup-" + System.currentTimeMillis();
+    }
+
+    @Test
+    public void testOneGroup() throws Exception
+    {
+        // do everything as owner
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    Group expectGroup = new Group(getGroupID(), daoTestUser1);
+                    Group actualGroup = getGroupDAO().addGroup(expectGroup);
+                    log.debug("addGroup: " + expectGroup.getID());
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    Group otherGroup = new Group(getGroupID(), daoTestUser1);
+                    otherGroup = getGroupDAO().addGroup(otherGroup);
+                    log.debug("addGroup: " + otherGroup.getID());
+
+                    // modify group fields
+                    // description
+                    expectGroup.description = "Happy testing";
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    expectGroup.description = null;
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+
+                    // userMembers
+                    expectGroup.getUserMembers().add(daoTestUser2);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+
+                    expectGroup.getUserMembers().remove(daoTestUser2);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    // groupMembers
+                    expectGroup.getGroupMembers().add(otherGroup);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    expectGroup.getGroupMembers().remove(otherGroup);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+
+                    expectGroup.description = "Happy testing";
+                    expectGroup.getUserMembers().add(daoTestUser2);
+                    expectGroup.getGroupMembers().add(otherGroup);
+
+                    // userAdmins
+                    expectGroup.getUserAdmins().add(daoTestUser3);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+
+                    // groupAdmins
+                    Group adminGroup = new Group(getGroupID(), daoTestUser1);
+                    adminGroup = getGroupDAO().addGroup(adminGroup);
+                    expectGroup.getGroupAdmins().add(adminGroup);
+                    actualGroup = getGroupDAO().modifyGroup(expectGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+
+                    // delete the group
+                    getGroupDAO().deleteGroup(expectGroup.getID());
+                    try
+                    {
+                        getGroupDAO().getGroup(expectGroup.getID());
+                        fail("get on deleted group should throw exception");
+                    }
+                    catch (GroupNotFoundException ignore) {}
+                    
+                    // reactivate the group
+                    actualGroup = getGroupDAO().addGroup(expectGroup);
+                    assertTrue(actualGroup instanceof ActivatedGroup);
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    // get the activated group
+                    actualGroup = getGroupDAO().getGroup(expectGroup.getID());
+                    assertGroupsEqual(expectGroup, actualGroup);
+                    
+                    // delete the group
+                    getGroupDAO().deleteGroup(expectGroup.getID());
+                    
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    e.printStackTrace();
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+    }
+    
+    @Test
+    public void testSearchOwnerGroups() throws Exception
+    {
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    String groupID = getGroupID();
+                    Group testGroup = new Group(groupID, daoTestUser1);
+                    testGroup = getGroupDAO().addGroup(testGroup);
+                    
+                    Collection<Group> groups = 
+                            getGroupDAO().getGroups(daoTestUser1.getUserID(), 
+                                                    Role.OWNER, null);
+                    assertNotNull(groups);
+                    
+                    boolean found = false;
+                    for (Group group : groups)
+                    {
+                        if (group.getID().equals(group.getID()))
+                        {
+                            found = true;
+                        }
+                    }
+                    if (!found)
+                    {
+                        fail("Group for owner not found");
+                    }
+                    
+                    groups = getGroupDAO().getGroups(daoTestUser1.getUserID(), 
+                                                     Role.OWNER, groupID);
+                    assertNotNull(groups);
+                    assertEquals(1, groups.size());
+                    assertTrue(groups.iterator().next().equals(testGroup));
+                    
+                    getGroupDAO().deleteGroup(groupID);
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+    }
+    
+    @Test
+    public void testSearchMemberGroups() throws Exception
+    {
+        final String groupID = getGroupID();
+        final String testGroup1ID = groupID + ".1";
+        final String testGroup2ID = groupID + ".2";
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    Group testGroup1 = new Group(testGroup1ID, daoTestUser1);
+                    testGroup1.getUserMembers().add(daoTestUser2);
+                    testGroup1 = getGroupDAO().addGroup(testGroup1);
+                    log.debug("add group: " + testGroup1ID);
+                    
+                    Group testGroup2 = new Group(testGroup2ID, daoTestUser1);
+                    testGroup2.getUserMembers().add(daoTestUser2);
+                    testGroup2 = getGroupDAO().addGroup(testGroup2);
+                    log.debug("add group: " + testGroup2ID);
+                    Thread.sleep(1000); //sleep to let memberof plugin in LDAP do its work 
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser2Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    Collection<Group> groups = 
+                            getGroupDAO().getGroups(daoTestUser2.getUserID(), 
+                                                    Role.MEMBER, null);
+                    
+                    assertNotNull(groups);
+                    assertTrue(groups.size() >= 2);
+                    
+                    log.debug("testSearchMemberGroups groups found: " + groups.size());
+                    boolean found1 = false;
+                    boolean found2 = false;
+                    for (Group group : groups)
+                    {
+                        log.debug("member group: " + group.getID());
+                        if (group.getID().equals(testGroup1ID))
+                        {
+                            found1 = true;
+                        }
+                        if (group.getID().equals(testGroup2ID))
+                        {
+                            found2 = true;
+                        }
+                    }
+                    if (!found1)
+                    {
+                        fail("Test group 1 not found");
+                    }
+                    if (!found2)
+                    {
+                        fail("Test group 2 not found");
+                    }
+                    
+                    groups = getGroupDAO().getGroups(daoTestUser2.getUserID(), 
+                                                     Role.MEMBER, testGroup1ID);
+                    assertNotNull(groups);
+                    assertTrue(groups.size() == 1);
+                    assertTrue(groups.iterator().next().getID().equals(testGroup1ID));
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    getGroupDAO().deleteGroup(testGroup1ID);
+                    getGroupDAO().deleteGroup(testGroup2ID);                    
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+    }
+
+    @Test
+    public void testSearchAdminGroups() throws Exception
+    {
+        final String groupID = getGroupID();
+        final String testGroup1ID = groupID + ".1";
+        final String testGroup2ID = groupID + ".2";
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    Group testGroup1 = new Group(testGroup1ID, daoTestUser1);
+                    testGroup1.getUserAdmins().add(daoTestUser2);
+                    testGroup1 = getGroupDAO().addGroup(testGroup1);
+                    log.debug("add group: " + testGroup1ID);
+                    
+                    Group testGroup2 = new Group(testGroup2ID, daoTestUser1);
+                    testGroup2.getUserAdmins().add(daoTestUser2);
+                    testGroup2 = getGroupDAO().addGroup(testGroup2);
+                    log.debug("add group: " + testGroup2ID);
+                    Thread.sleep(1000); // sleep to let memberof plugin do its work
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser2Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                       
+                    Collection<Group> groups = 
+                            getGroupDAO().getGroups(daoTestUser2.getUserID(), 
+                                                    Role.ADMIN, null);
+                    
+                    log.debug("testSearchAdminGroups groups found: " + groups.size());
+                    assertNotNull(groups);
+                    assertTrue(groups.size() >= 2);
+                    
+                    boolean found1 = false;
+                    boolean found2 = false;
+                    for (Group group : groups)
+                    {
+                        log.debug("admin group: " + group.getID());
+                        if (group.getID().equals(testGroup1ID))
+                        {
+                            found1 = true;
+                        }
+                        if (group.getID().equals(testGroup2ID))
+                        {
+                            found2 = true;
+                        }
+                    }
+                    if (!found1)
+                    {
+                        fail("Admin group " + testGroup1ID + " not found");
+                    }
+                    if (!found2)
+                    {
+                        fail("Admin group " + testGroup2ID + " not found");
+                    }
+                    
+                    groups = getGroupDAO().getGroups(daoTestUser2.getUserID(), 
+                                                     Role.ADMIN, testGroup1ID);
+                    assertNotNull(groups);
+                    assertTrue(groups.size() == 1);
+                    assertTrue(groups.iterator().next().getID().equals(testGroup1ID));
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    getGroupDAO().deleteGroup(testGroup1ID);
+                    getGroupDAO().deleteGroup(testGroup2ID);                    
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+    }
+
+    @Test
+    public void testGetGroupNames() throws Exception
+    {
+        final String groupID = getGroupID();
+        final String testGroup1ID = groupID + ".1";
+        final String testGroup2ID = groupID + ".2";
+
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    Group testGroup1 = new Group(testGroup1ID, daoTestUser1);
+                    testGroup1 = getGroupDAO().addGroup(testGroup1);
+                    log.debug("add group: " + testGroup1ID);
+
+                    Group testGroup2 = new Group(testGroup2ID, daoTestUser1);
+                    testGroup2 = getGroupDAO().addGroup(testGroup2);
+                    log.debug("add group: " + testGroup2ID);
+                    //Thread.sleep(1000); // sleep to let memberof plugin do its work
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    Collection<String> groups = getGroupDAO().getGroupNames();
+
+                    log.debug("testGetGroupNames groups found: " + groups.size());
+                    assertNotNull(groups);
+                    assertTrue(groups.size() >= 2);
+
+                    boolean found1 = false;
+                    boolean found2 = false;
+                    for (String name : groups)
+                    {
+                        log.debug("group: " + name);
+                        if (name.equals(testGroup1ID))
+                        {
+                            found1 = true;
+                        }
+                        if (name.equals(testGroup2ID))
+                        {
+                            found2 = true;
+                        }
+                    }
+                    if (!found1)
+                    {
+                        fail("Admin group " + testGroup1ID + " not found");
+                    }
+                    if (!found2)
+                    {
+                        fail("Admin group " + testGroup2ID + " not found");
+                    }
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    getGroupDAO().deleteGroup(testGroup1ID);
+                    getGroupDAO().deleteGroup(testGroup2ID);
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+                return null;
+            }
+        });
+    }
+    
+    @Test
+    public void testAddGroupExceptions() throws Exception
+    {
+        Subject.doAs(anonSubject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().addGroup(new Group(getGroupID(), daoTestUser1));
+                    fail("addGroup with anonymous access should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().addGroup(new Group("foo", unknownUser));
+                    fail("addGroup with unknown user should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                
+                Group group = getGroupDAO().addGroup(new Group(getGroupID(), 
+                                                     daoTestUser1));
+                
+                try
+                {
+                    getGroupDAO().addGroup(group);
+                    fail("addGroup with existing group should throw " + 
+                         "GroupAlreadyExistsException");
+                }
+                catch (GroupAlreadyExistsException ignore) {}
+                
+                getGroupDAO().deleteGroup(group.getID());
+                return null;
+            }
+        });
+    }
+    
+    @Test
+    public void testGetGroupExceptions() throws Exception
+    {
+        final String groupID = getGroupID();
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().getGroup(groupID);
+                    fail("getGroup with unknown group should throw " + 
+                         "GroupNotFoundException");
+                }
+                catch (GroupNotFoundException ignore) {}
+                
+                getGroupDAO().addGroup(new Group(groupID, daoTestUser1));
+                return null;
+            }
+        });
+
+        Subject.doAs(anonSubject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().getGroup(groupID);
+                    fail("getGroup with anonymous access should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().getGroup(groupID);
+                    //fail("getGroup with anonymous access should throw " + 
+                    //     "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+
+        // All access ACI's will allow anonymous access
+//        Subject.doAs(daoTestUser2Subject, new PrivilegedExceptionAction<Object>()
+//        {
+//            public Object run() throws Exception
+//            {
+//                try
+//                {
+//                    getGroupDAO().getGroup(groupID);
+//                    fail("getGroup with anonymous access should throw " +
+//                         "AccessControlException");
+//                }
+//                catch (AccessControlException ignore) {}
+//                return null;
+//            }
+//        });
+    }
+
+    @Test
+    public void testModifyGroupExceptions() throws Exception
+    {        
+        final String groupID = getGroupID();
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                getGroupDAO().addGroup(new Group(groupID, daoTestUser1));
+                try
+                {
+                    getGroupDAO().modifyGroup(new Group("foo", daoTestUser1));
+                    fail("modifyGroup with unknown user should throw " + 
+                         "GroupNotFoundException");
+                }
+                catch (GroupNotFoundException ignore) {}
+
+                return null;
+            }
+        });
+
+        Subject.doAs(anonSubject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().getGroup(groupID);
+                    fail("getGroup with anonymous access should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {               
+                getGroupDAO().deleteGroup(groupID);
+                return null;
+            }
+        });
+    }
+    
+    @Test
+    public void testDeleteGroupExceptions() throws Exception
+    {
+        final String groupID = getGroupID();
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().deleteGroup(groupID);
+                    fail("deleteGroup with unknown group should throw " + 
+                         "GroupNotFoundException");
+                }
+                catch (GroupNotFoundException ignore) {}
+                
+                getGroupDAO().addGroup(new Group(groupID, daoTestUser1));
+                return null;
+            }
+        });
+
+        Subject.doAs(anonSubject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().deleteGroup(groupID);
+                    fail("deleteGroup with anonymous access should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {                
+                getGroupDAO().deleteGroup(groupID);
+                return null;
+            }
+        });
+    }
+    
+    @Test
+    public void testSearchGroupsExceptions() throws Exception
+    {        
+        final String groupID = getGroupID();
+        
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                getGroupDAO().addGroup(new Group(groupID, daoTestUser1));
+                
+                try
+                {
+                    getGroupDAO().getGroups(unknownPrincipal, Role.OWNER, 
+                                               groupID);
+                    fail("searchGroups with unknown user should throw " + 
+                         "UserNotFoundException");
+                }
+                catch (AccessControlException ignore) {}
+                
+                try
+                {
+                    getGroupDAO().getGroups(daoTestPrincipal1, Role.OWNER, 
+                                               "foo");
+                    fail("searchGroups with unknown user should throw " + 
+                         "GroupNotFoundException");
+                }
+                catch (GroupNotFoundException ignore) {}
+                return null;
+            }
+        });
+
+        Subject.doAs(anonSubject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {                    
+                    getGroupDAO().getGroups(daoTestPrincipal1, Role.OWNER, 
+                                               groupID);
+                    fail("searchGroups with anonymous access should throw " + 
+                         "AccessControlException");
+                }
+                catch (AccessControlException ignore) {}
+                return null;
+            }
+        });
+
+        //
+        // change the user
+//        Subject.doAs(daoTestUser2Subject, new PrivilegedExceptionAction<Object>()
+//        {
+//            public Object run() throws Exception
+//            {
+//                try
+//                {
+//                    Group group = getGroupDAO().getGroup(groupID);
+//                    assertTrue(group == null);
+//
+//                    fail("searchGroups with un-authorized user should throw " +
+//                         "AccessControlException");
+//                }
+//                catch (AccessControlException ignore)
+//                {
+//
+//                }
+//
+//                return null;
+//            }
+//        });
+
+        Subject.doAs(daoTestUser1Subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                getGroupDAO().deleteGroup(groupID);
+                return null;
+            }
+        });
+    }
+
+    private void assertGroupsEqual(Group gr1, Group gr2)
+    {
+        if ((gr1 == null) && (gr2 == null))
+        {
+            return;
+        }
+        assertEquals(gr1, gr2);
+        assertEquals(gr1.getID(), gr2.getID());
+        assertEquals(gr1.description, gr2.description);
+        assertEquals(gr1.getOwner(), gr2.getOwner());
+
+        assertEquals(gr1.getGroupMembers(), gr2.getGroupMembers());
+        assertEquals(gr1.getGroupMembers().size(), gr2.getGroupMembers().size());
+        for (Group gr : gr1.getGroupMembers())
+        {
+            assertTrue(gr2.getGroupMembers().contains(gr));
+        }
+
+        assertEquals(gr1.getUserMembers(), gr2.getUserMembers());
+        assertEquals(gr1.getUserMembers().size(), gr2.getUserMembers()
+                .size());
+        for (User<?> user : gr1.getUserMembers())
+        {
+            assertTrue(gr2.getUserMembers().contains(user));
+        }
+
+        assertEquals(gr1.getGroupAdmins(), gr2.getGroupAdmins());
+        assertEquals(gr1.getGroupAdmins().size(), gr2.getGroupAdmins().size());
+        for (Group gr : gr1.getGroupAdmins())
+        {
+            assertTrue(gr2.getGroupAdmins().contains(gr));
+        }
+
+        assertEquals(gr1.getUserAdmins(), gr2.getUserAdmins());
+        assertEquals(gr1.getUserAdmins().size(), gr2.getUserAdmins()
+                .size());
+        for (User<?> user : gr1.getUserAdmins())
+        {
+            assertTrue(gr2.getUserAdmins().contains(user));
+        }
+
+        assertEquals(gr1.getProperties(), gr2.getProperties());
+        for (GroupProperty prop : gr1.getProperties())
+        {
+            assertTrue(gr2.getProperties().contains(prop));
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..abce3f7810d0941c878c2ea03a98de9c093dfc00
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/ldap/LdapUserDAOTest.java
@@ -0,0 +1,311 @@
+/*
+ ************************************************************************
+ *******************  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.server.ldap;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.util.Collection;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import ca.nrc.cadc.ac.PersonalDetails;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.UserDetails;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.util.Log4jInit;
+
+import com.unboundid.ldap.sdk.DN;
+
+public class LdapUserDAOTest extends AbstractLdapDAOTest
+{
+    private static final Logger log = Logger.getLogger(LdapUserDAOTest.class);
+
+    static final String testUserX509DN = "cn=cadcdaotest1,ou=cadc,o=hia,c=ca";
+
+    static String testUserDN;
+    static User<X500Principal> testUser;
+    static LdapConfig config;
+    
+    @BeforeClass
+    public static void setUpBeforeClass()
+        throws Exception
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+
+        // get the configuration of the development server from and config files...
+        config = getLdapConfig();
+
+        testUser = new User<X500Principal>(new X500Principal(testUserX509DN));
+        testUser.details.add(new PersonalDetails("CADC", "DAOTest1"));
+        testUser.getIdentities().add(new HttpPrincipal("CadcDaoTest1"));
+
+        testUserDN = "uid=cadcdaotest1," + config.getUsersDN();
+    }
+
+    LdapUserDAO<X500Principal> getUserDAO()
+    {
+        return new LdapUserDAO<X500Principal>(config);
+    }
+    
+    /**
+     * Test of getUser method, of class LdapUserDAO.
+     */
+    @Test
+    public void testGetUser() throws Exception
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser.getUserID());
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {
+                    User<X500Principal> actual = getUserDAO().getUser(testUser.getUserID());
+                    check(testUser, actual);
+                    
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+
+    }
+
+    /**
+     * Test of getUserGroups method, of class LdapUserDAO.
+     */
+    @Test
+    public void testGetUserGroups() throws Exception
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser.getUserID());
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {            
+                    Collection<DN> groups = getUserDAO().getUserGroups(testUser.getUserID(), false);
+                    assertNotNull(groups);
+                    assertTrue(!groups.isEmpty());
+                    for (DN groupDN : groups)
+                        log.debug(groupDN);
+                    
+                    groups = getUserDAO().getUserGroups(testUser.getUserID(), true);
+                    assertNotNull(groups);
+                    assertTrue(!groups.isEmpty());
+                    for (DN groupDN : groups)
+                        log.debug(groupDN);
+                    
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+    }
+    
+    /**
+     * Test of getUserGroups method, of class LdapUserDAO.
+     */
+    @Test
+    public void testIsMember() throws Exception
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser.getUserID());
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    boolean isMember = getUserDAO().isMember(testUser.getUserID(), "foo");
+                    assertFalse(isMember);
+                    
+                    String groupDN = "cn=cadcdaotestgroup1," + config.getGroupsDN();
+                    isMember = getUserDAO().isMember(testUser.getUserID(), groupDN);
+                    assertTrue(isMember);
+                    
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+    }
+    
+    /**
+     * Test of getMember.
+     */
+    @Test
+    public void testGetMember() throws Exception
+    {
+        Subject subject = new Subject();
+        subject.getPrincipals().add(testUser.getUserID());
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
+                    check(testUser, actual);
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+        
+        // should also work as a different user
+        subject = new Subject();
+        subject.getPrincipals().add(new HttpPrincipal("CadcDaoTest2"));
+
+        // do everything as owner
+        Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+        {
+            public Object run() throws Exception
+            {
+                try
+                {   
+                    User<X500Principal> actual = getUserDAO().getMember(new DN(testUserDN));
+                    check(testUser, actual);
+                    return null;
+                }
+                catch (Exception e)
+                {
+                    throw new Exception("Problems", e);
+                }
+            }
+        });
+    }
+    
+    private static void check(final User<? extends Principal> user1, final User<? extends Principal> user2)
+    {
+        assertEquals(user1, user2);
+        assertEquals(user1.details, user2.details);
+        assertEquals(user1.details.size(), user2.details.size());
+        assertEquals(user1.getIdentities(), user2.getIdentities());
+        for(UserDetails d1 : user1.details)
+        {
+            assertTrue(user2.details.contains(d1));
+            if(d1 instanceof PersonalDetails)
+            {
+                PersonalDetails pd1 = (PersonalDetails)d1;
+                boolean found = false;
+                for(UserDetails d2 : user2.details)
+                {
+                    if(d2 instanceof PersonalDetails)
+                    {
+                        PersonalDetails pd2 = (PersonalDetails)d2;
+                        assertEquals(pd1, pd2); // already done in contains above but just in case
+                        assertEquals(pd1.address, pd2.address);
+                        assertEquals(pd1.city, pd2.city);
+                        assertEquals(pd1.country, pd2.country);
+                        assertEquals(pd1.email, pd2.email);
+                        assertEquals(pd1.institute, pd2.institute);
+                        found = true;
+                    }
+                    assertTrue(found);
+                }
+            }
+        }
+        
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..56fa6154db4f7a2ed1629abfa28de4eef024e7ab
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddGroupMemberActionTest.java
@@ -0,0 +1,173 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.Principal;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class AddGroupMemberActionTest
+{
+    private final static Logger log = Logger.getLogger(AddGroupMemberActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    public void testExceptions()
+    {
+        try
+        {
+            Group group = new Group("group", null);
+            Group member = new Group("member", null);
+            group.getGroupMembers().add(member);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
+            EasyMock.replay(groupPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            AddGroupMemberAction action = new AddGroupMemberAction(logInfo, "group", "member")
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+            };
+            
+            try
+            {
+                action.run();
+                fail("duplicate group member should throw GroupAlreadyExistsException");
+            }
+            catch (GroupAlreadyExistsException ignore) {}
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+    @Test
+    public void testRun() throws Exception
+    {
+        try
+        {
+            Group group = new Group("group", null);
+            Group member = new Group("member", null);
+            Group modified = new Group("group", null);
+            modified.getGroupMembers().add(member);
+            
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
+            EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
+            EasyMock.replay(groupPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            AddGroupMemberAction action = new AddGroupMemberAction(logInfo, "group", "member")
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+            };
+
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..6488fd768583dd7d06841a16547158a6c8b32b6a
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/AddUserMemberActionTest.java
@@ -0,0 +1,205 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.Principal;
+import javax.security.auth.x500.X500Principal;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class AddUserMemberActionTest
+{
+    private final static Logger log = Logger.getLogger(AddUserMemberActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testExceptions()
+    {
+        try
+        {   
+            String userID = "foo";
+            String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
+            Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
+            User<Principal> user = new User<Principal>(userPrincipal);
+            
+            Group group = new Group("group", null);
+            group.getUserMembers().add(user);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.replay(groupPersistence);
+            
+            final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
+            EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
+            EasyMock.replay(userPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            AddUserMemberAction action = new AddUserMemberAction(logInfo, "group", userID, userIDType)
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+                
+                @Override
+                <T extends Principal> UserPersistence<T> getUserPersistence()
+                {
+                    return userPersistence;
+                };
+            };
+            
+            try
+            {
+                action.run();
+                fail("duplicate group member should throw MemberAlreadyExistsException");
+            }
+            catch (MemberAlreadyExistsException ignore) {}
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testRun() throws Exception
+    {
+        try
+        {
+            String userID = "foo";
+            String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
+            Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
+            User<Principal> user = new User<Principal>(userPrincipal);
+            
+            Group group = new Group("group", null);
+            Group modified = new Group("group", null);
+            modified.getUserMembers().add(user);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
+            EasyMock.replay(groupPersistence);
+            
+            final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
+            EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
+            EasyMock.replay(userPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            AddUserMemberAction action = new AddUserMemberAction(logInfo, "group", userID, userIDType)
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+                
+                @Override
+                <T extends Principal> UserPersistence<T> getUserPersistence()
+                {
+                    return userPersistence;
+                };
+            };
+            
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4562671a6951e209e759a96993499782ede5a9e8
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/DeleteGroupActionTest.java
@@ -0,0 +1,136 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.Principal;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class DeleteGroupActionTest
+{
+   private final static Logger log = Logger.getLogger(DeleteGroupActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    public void testRun()
+    {
+        try
+        {   
+            Group group = new Group("group", null);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            groupPersistence.deleteGroup("group");
+            EasyMock.expectLastCall().once();
+            EasyMock.replay(groupPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            DeleteGroupAction action = new DeleteGroupAction(logInfo, "group")
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+            };
+
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0cb59383950ce877c3c5c88a4ee1da506adecce7
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GetGroupNamesActionTest.java
@@ -0,0 +1,159 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.util.Log4jInit;
+import ca.nrc.cadc.uws.server.SyncOutput;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static org.junit.Assert.fail;
+
+/**
+ *
+ * @author jburke
+ */
+public class GetGroupNamesActionTest
+{
+    private final static Logger log = Logger.getLogger(GetGroupNamesActionTest.class);
+
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    @Ignore
+    public void testRun() throws Exception
+    {
+        try
+        {
+            Collection<String> groupNames = new ArrayList<String>();
+            groupNames.add("foo");
+            groupNames.add("bar");
+
+            final GroupPersistence mockPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(mockPersistence.getGroupNames()).andReturn(groupNames).once();
+
+            final PrintWriter mockWriter = EasyMock.createMock(PrintWriter.class);
+            mockWriter.write("foo", 0, 3);
+            EasyMock.expectLastCall();
+            mockWriter.write(44);
+            EasyMock.expectLastCall();
+            mockWriter.write("bar", 0, 3);
+            EasyMock.expectLastCall();
+            mockWriter.write("\n");
+            EasyMock.expectLastCall();
+
+            final SyncOutput mockSyncOutput =
+                    EasyMock.createMock(SyncOutput.class);
+
+            mockSyncOutput.setHeader("Content-Type", "text/csv");
+
+            final HttpServletResponse mockResponse = EasyMock.createMock(HttpServletResponse.class);
+            mockResponse.setContentType("text/csv");
+            EasyMock.expectLastCall();
+            EasyMock.expect(mockResponse.getWriter()).andReturn(mockWriter).once();
+
+            GroupLogInfo mockLog = EasyMock.createMock(GroupLogInfo.class);
+
+            EasyMock.replay(mockPersistence, mockWriter, mockResponse, mockLog);
+
+            GetGroupNamesAction action = new GetGroupNamesAction(mockLog)
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return mockPersistence;
+                };
+            };
+
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f62d030cc6b0f9131a9681250664fbee8a97444c
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupActionFactoryTest.java
@@ -0,0 +1,369 @@
+/**
+ ************************************************************************
+ *******************  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/>.
+ *
+ ************************************************************************
+ */
+
+package ca.nrc.cadc.ac.server.web;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import ca.nrc.cadc.util.Log4jInit;
+
+import java.net.URL;
+
+public class GroupActionFactoryTest
+{
+    private final static Logger log = Logger.getLogger(GroupActionFactoryTest.class);
+
+    public GroupActionFactoryTest()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    public void testCreateAddGroupMemberAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName/groupMembers/groupToAdd");
+            EasyMock.expect(request.getMethod()).andReturn("PUT");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof AddGroupMemberAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateAddUserMemberAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName/userMembers/userToAdd");
+            EasyMock.expect(request.getParameter("idType")).andReturn("x509");
+            EasyMock.expect(request.getMethod()).andReturn("PUT");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof AddUserMemberAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateCreateGroupAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("");
+            EasyMock.expect(request.getMethod()).andReturn("PUT");
+            EasyMock.expect(request.getInputStream()).andReturn(null);
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof CreateGroupAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateDeleteGroupAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName");
+            EasyMock.expect(request.getMethod()).andReturn("DELETE");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof DeleteGroupAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateGetGroupAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName");
+            EasyMock.expect(request.getMethod()).andReturn("GET");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof GetGroupAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateGetGroupNamesAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("");
+            EasyMock.expect(request.getMethod()).andReturn("GET");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof GetGroupNamesAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateModifyGroupAction()
+    {
+        try
+        {
+            StringBuffer sb = new StringBuffer();
+            sb.append("http://localhost:80/ac/groups/foo");
+
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName");
+            EasyMock.expect(request.getMethod()).andReturn("POST");
+            EasyMock.expect(request.getRequestURL()).andReturn(sb);
+            EasyMock.expect(request.getContextPath()).andReturn("");
+            EasyMock.expect(request.getServletPath()).andReturn("");
+            EasyMock.expect(request.getInputStream()).andReturn(null);
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof ModifyGroupAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateRemoveGroupMemberAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName/groupMembers/groupToRenove");
+            EasyMock.expect(request.getMethod()).andReturn("DELETE");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof RemoveGroupMemberAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testCreateRemoveUserMemberAction()
+    {
+        try
+        {
+            HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+            EasyMock.expect(request.getPathInfo()).andReturn("groupName/userMembers/userToRemove");
+            EasyMock.expect(request.getMethod()).andReturn("DELETE");
+            EasyMock.expect(request.getParameter("idType")).andReturn("x509");
+            EasyMock.replay(request);
+            GroupsAction action = GroupsActionFactory.getGroupsAction(request, null);
+            EasyMock.verify(request);
+            Assert.assertTrue("Wrong action", action instanceof RemoveUserMemberAction);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    @Test
+    public void testBadRequests()
+    {
+        try
+        {
+            TestRequest[] testRequests =
+            {
+                new TestRequest("", "POST"),
+                new TestRequest("", "DELETE"),
+                new TestRequest("", "HEAD"),
+                new TestRequest("groupName/groupMembers", "GET"),
+                new TestRequest("groupName/groupMembers", "POST"),
+                new TestRequest("groupName/groupMembers", "PUT"),
+                new TestRequest("groupName/groupMembers", "DELETE"),
+                new TestRequest("groupName/groupMembers", "HEAD"),
+                new TestRequest("groupName/misspelled/id", "GET"),
+                new TestRequest("groupName/groupMembers/groupMemberName", "GET"),
+                new TestRequest("groupName/groupMembers/groupMemberName", "POST"),
+                new TestRequest("groupName/groupMembers/groupMemberName", "HEAD"),
+                new TestRequest("groupName/userMembers/userMemberName", "GET"),
+                new TestRequest("groupName/userMembers/userMemberName", "POST"),
+                new TestRequest("groupName/userMembers/userMemberName", "HEAD"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "GET"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "POST"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "PUT"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "HEAD"),
+                new TestRequest("groupName/groupMembers/groupMemberName/tooManySegments", "DELETE"),
+            };
+
+            for (TestRequest testRequest : testRequests)
+            {
+
+                log.debug("Testing: " + testRequest);
+
+                HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class);
+                EasyMock.expect(request.getPathInfo()).andReturn(testRequest.path);
+                EasyMock.expect(request.getMethod()).andReturn(testRequest.method);
+                if (testRequest.paramName != null)
+                {
+                    EasyMock.expect(request.getParameter(testRequest.paramName)).andReturn(testRequest.paramValue);
+                }
+                EasyMock.replay(request);
+                try
+                {
+                    GroupsActionFactory.getGroupsAction(request, null);
+                    Assert.fail("Should have been a bad request: " + testRequest.method + " on " + testRequest.path);
+                }
+                catch (IllegalArgumentException e)
+                {
+                    // expected
+                }
+                EasyMock.verify(request);
+            }
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            Assert.fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    private class TestRequest
+    {
+        public String path;
+        public String method;
+        public String paramName;
+        public String paramValue;
+
+        public TestRequest(String path, String method)
+        {
+            this(path, method, null, null);
+        }
+        public TestRequest(String path, String method, String paramName, String paramValue)
+        {
+            this.path = path;
+            this.method = method;
+            this.paramName = paramName;
+            this.paramValue = paramValue;
+        }
+        @Override
+        public String toString()
+        {
+            return method + " on path " + path +
+                ((paramName == null) ? "" : "?" + paramName + "=" + paramValue);
+        }
+
+    }
+
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..08b28e9725675f4d955688e194198862144b9f42
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/GroupsActionTest.java
@@ -0,0 +1,264 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.MemberNotFoundException;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.net.TransientException;
+import ca.nrc.cadc.util.Log4jInit;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.security.AccessControlException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class GroupsActionTest
+{
+    private final static Logger log = Logger.getLogger(GroupsActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    public void testDoActionAccessControlException() throws Exception
+    {
+        String message = "Permission Denied";
+        int responseCode = 403;
+        Exception e = new AccessControlException("");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionIllegalArgumentException() throws Exception
+    {
+        String message = "message";
+        int responseCode = 400;
+        Exception e = new IllegalArgumentException("message");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionMemberNotFoundException() throws Exception
+    {
+        String message = "Member not found: foo";
+        int responseCode = 404;
+        Exception e = new MemberNotFoundException("foo");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionGroupNotFoundException() throws Exception
+    {
+        String message = "Group not found: foo";
+        int responseCode = 404;
+        Exception e = new GroupNotFoundException("foo");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionUserNotFoundException() throws Exception
+    {
+        String message = "User not found: foo";
+        int responseCode = 404;
+        Exception e = new UserNotFoundException("foo");
+        testDoAction(message, responseCode, e);
+    }
+
+    @Test
+    public void testDoActionMemberAlreadyExistsException() throws Exception
+    {
+        String message = "Member already exists: foo";
+        int responseCode = 409;
+        Exception e = new MemberAlreadyExistsException("foo");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionGroupAlreadyExistsException() throws Exception
+    {
+        String message = "Group already exists: foo";
+        int responseCode = 409;
+        Exception e = new GroupAlreadyExistsException("foo");
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionUnsupportedOperationException() throws Exception
+    {
+        String message = "Not yet implemented.";
+        int responseCode = 501;
+        Exception e = new UnsupportedOperationException();
+        testDoAction(message, responseCode, e);
+    }
+    
+    @Test
+    public void testDoActionTransientException() throws Exception
+    {
+        try
+        {
+            HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
+            EasyMock.expect(response.isCommitted()).andReturn(Boolean.FALSE);
+            response.setContentType("text/plain");
+            EasyMock.expectLastCall().once();
+            EasyMock.expect(response.getWriter()).andReturn(new PrintWriter(new StringWriter()));
+            EasyMock.expectLastCall().once();
+            response.setStatus(503);
+            EasyMock.expectLastCall().once();
+            EasyMock.replay(response);
+
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            logInfo.setSuccess(false);
+            EasyMock.expectLastCall().once();
+            logInfo.setMessage("Internal Transient Error: foo");
+            EasyMock.expectLastCall().once();
+            EasyMock.replay(logInfo);
+
+            GroupsActionImpl action = new GroupsActionImpl(logInfo);
+            action.setException(new TransientException("foo"));
+            action.doAction(null, response);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+    private void testDoAction(String message, int responseCode, Exception e)
+        throws Exception
+    {
+        try
+        {
+            HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
+            EasyMock.expect(response.isCommitted()).andReturn(Boolean.FALSE);
+            response.setContentType("text/plain");
+            EasyMock.expectLastCall().once();
+            EasyMock.expect(response.getWriter()).andReturn(new PrintWriter(new StringWriter()));
+            EasyMock.expectLastCall().once();
+            response.setStatus(responseCode);
+            EasyMock.expectLastCall().once();
+            EasyMock.replay(response);
+
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            logInfo.setMessage(message);
+            EasyMock.expectLastCall().once();
+            EasyMock.replay(logInfo);
+
+            GroupsActionImpl action = new GroupsActionImpl(logInfo);
+            action.setException(e);
+            action.doAction(null, response);
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+
+    public class GroupsActionImpl extends GroupsAction
+    {
+        Exception exception;
+        
+        public GroupsActionImpl(GroupLogInfo logInfo)
+        {
+            super(logInfo);
+        }
+
+        public Object run() throws Exception
+        {
+            throw exception;
+        }
+
+        public void setException(Exception e)
+        {
+            this.exception = e;
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ccc5abb9753fd6ad5f1336e1ddfd4b46d8c1638
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveGroupMemberActionTest.java
@@ -0,0 +1,176 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.GroupAlreadyExistsException;
+import ca.nrc.cadc.ac.GroupNotFoundException;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.Principal;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class RemoveGroupMemberActionTest
+{
+    private final static Logger log = Logger.getLogger(RemoveGroupMemberActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    public void testExceptions()
+    {
+        try
+        {
+            Group group = new Group("group", null);
+            Group member = new Group("member", null);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
+            EasyMock.replay(groupPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            RemoveGroupMemberAction action = new RemoveGroupMemberAction(logInfo, "group", "member")
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+            };
+            
+            try
+            {
+                action.run();
+                fail("unknown group member should throw GroupNotFoundException");
+            }
+            catch (GroupNotFoundException ignore) {}
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+    @Test
+    public void testRun() throws Exception
+    {
+        try
+        {
+            Group member = new Group("member", null);
+            Group group = new Group("group", null);
+            group.getGroupMembers().add(member);
+            
+            Group modified = new Group("group", null);
+            modified.getGroupMembers().add(member);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.getGroup("member")).andReturn(member);
+            EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
+            EasyMock.replay(groupPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            RemoveGroupMemberAction action = new RemoveGroupMemberAction(logInfo, "group", "member")
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+            };
+
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+}
diff --git a/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..fea4de56d5d4ff31e3f08e473eeb5d8f8111fc12
--- /dev/null
+++ b/projects/cadcAccessControl-Server/test/src/ca/nrc/cadc/ac/server/web/RemoveUserMemberActionTest.java
@@ -0,0 +1,206 @@
+/*
+ ************************************************************************
+ *******************  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.server.web;
+
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.MemberAlreadyExistsException;
+import ca.nrc.cadc.ac.MemberNotFoundException;
+import ca.nrc.cadc.ac.User;
+import ca.nrc.cadc.ac.server.GroupPersistence;
+import ca.nrc.cadc.ac.server.UserPersistence;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.util.Log4jInit;
+import java.security.Principal;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class RemoveUserMemberActionTest
+{
+   private final static Logger log = Logger.getLogger(RemoveUserMemberActionTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testExceptions()
+    {
+        try
+        {   
+            String userID = "foo";
+            String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
+            Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
+            User<Principal> user = new User<Principal>(userPrincipal);
+            
+            Group group = new Group("group", null);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.replay(groupPersistence);
+            
+            final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
+            EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
+            EasyMock.replay(userPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            RemoveUserMemberAction action = new RemoveUserMemberAction(logInfo, "group", userID, userIDType)
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+                
+                @Override
+                <T extends Principal> UserPersistence<T> getUserPersistence()
+                {
+                    return userPersistence;
+                };
+            };
+            
+            try
+            {
+                action.run();
+                fail("unknown group member should throw MemberNotFoundException");
+            }
+            catch (MemberNotFoundException ignore) {}
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testRun() throws Exception
+    {
+        try
+        {
+            String userID = "foo";
+            String userIDType = AuthenticationUtil.AUTH_TYPE_HTTP;
+            Principal userPrincipal = AuthenticationUtil.createPrincipal(userID, userIDType);
+            User<Principal> user = new User<Principal>(userPrincipal);
+            
+            Group group = new Group("group", null);
+            group.getUserMembers().add(user);
+            Group modified = new Group("group", null);
+            
+            final GroupPersistence groupPersistence = EasyMock.createMock(GroupPersistence.class);
+            EasyMock.expect(groupPersistence.getGroup("group")).andReturn(group);
+            EasyMock.expect(groupPersistence.modifyGroup(group)).andReturn(modified);
+            EasyMock.replay(groupPersistence);
+            
+            final UserPersistence userPersistence = EasyMock.createMock(UserPersistence.class);
+            EasyMock.expect(userPersistence.getUser(userPrincipal)).andReturn(user);
+            EasyMock.replay(userPersistence);
+            
+            GroupLogInfo logInfo = EasyMock.createMock(GroupLogInfo.class);
+            
+            RemoveUserMemberAction action = new RemoveUserMemberAction(logInfo, "group", userID, userIDType)
+            {
+                @Override
+                <T extends Principal> GroupPersistence<T> getGroupPersistence()
+                {
+                    return groupPersistence;
+                };
+                
+                @Override
+                <T extends Principal> UserPersistence<T> getUserPersistence()
+                {
+                    return userPersistence;
+                };
+            };
+            
+            action.run();
+        }
+        catch (Throwable t)
+        {
+            log.error(t.getMessage(), t);
+            fail("unexpected error: " + t.getMessage());
+        }
+    }
+    
+}
diff --git a/projects/cadcAccessControl/build.xml b/projects/cadcAccessControl/build.xml
index 25762598f1a728a912d6fe99a1a6c27352a4e9cf..a3b964ed3878c53e9c2aec9a667ebce0cc72ca79 100644
--- a/projects/cadcAccessControl/build.xml
+++ b/projects/cadcAccessControl/build.xml
@@ -8,7 +8,7 @@
 *  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
@@ -31,10 +31,10 @@
 *  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
@@ -44,7 +44,7 @@
 *  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
@@ -54,7 +54,7 @@
 *  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
@@ -67,31 +67,37 @@
 ************************************************************************
 -->
 
-
 <!DOCTYPE project>
 <project default="build" basedir=".">
     <property environment="env"/>
     <property file="local.build.properties" />
-    
+
     <!-- site-specific build properties or overrides of values in opencadc.properties -->
     <property file="${env.CADC_PREFIX}/etc/local.properties" />
-    
+
     <!-- site-specific targets, e.g. install, cannot duplicate those in opencadc.targets.xml -->
     <import file="${env.CADC_PREFIX}/etc/local.targets.xml" optional="true" />
 
     <!-- default properties and targets -->
     <property file="${env.CADC_PREFIX}/etc/opencadc.properties" />
     <import file="${env.CADC_PREFIX}/etc/opencadc.targets.xml"/>
-    
+
     <!-- developer convenience: place for extra targets and properties -->
     <import file="extras.xml" optional="true" />
 
-    <property name="project" value="cadcAccessControl" />
+    <property name="project"    value="cadcAccessControl" />
 
-    <property name="cadcUtil" value="${lib}/cadcUtil.jar" />
+    <property name="cadcUtil"           value="${lib}/cadcUtil.jar" />
+	<property name="cadcRegistryClient" value="${lib}/cadcRegistryClient.jar" />
+    
+    <property name="jdom2"      value="${ext.lib}/jdom2.jar" />
+    <property name="javacsv"      value="${ext.lib}/javacsv.jar" />
+    <property name="log4j"      value="${ext.lib}/log4j.jar" />
+    <property name="unboundid"  value="${ext.lib}/unboundid-ldapsdk-se.jar" />
 
-    <property name="jars" value="${cadcUtil}:${ext.lib}/log4j.jar" />
-  
+
+    <property name="jars" value="${jdom2}:${log4j}:${javacsv}:${unboundid}:${cadcUtil}:${cadcRegistryClient}" />
+    
     <target name="build" depends="compile">
         <jar jarfile="${build}/lib/${project}.jar"
                     basedir="${build}/class"
@@ -101,23 +107,13 @@
     </target>
 
     <!-- JAR files needed to run the test suite -->
-    <property name="testingJars" value="${build}/class:${jars}:${ext.lib}/junit.jar:${ext.lib}/xerces.jar:${ext.dev}/easymock.jar:${ext.dev}/cglib.jar:${ext.dev}/objenesis.jar:${ext.dev}/asm.jar" />
-
-    <target name="test" depends="compile-test">
-        <echo message="Running test" />
-
-        <!-- Run the junit test suite -->
-        <echo message="Running test suite..." />
-        <junit printsummary="yes" haltonfailure="yes" fork="yes">
-            <classpath>
-                <pathelement path="${build}/class"/>
-                <pathelement path="${build}/test/class"/>
-                <pathelement path="${testingJars}"/>
-            </classpath>
-            <test name="ca.nrc.cadc.ac.UserTest" />
-            <test name="ca.nrc.cadc.ac.GroupTest" />
-            <formatter type="plain" usefile="false" />
-        </junit>
-    </target>
+    <property name="xerces"     value="${ext.lib}/xerces.jar" />
+    <property name="asm"        value="${ext.dev}/asm.jar" />
+    <property name="cglib"      value="${ext.dev}/cglib.jar" />
+    <property name="easymock"   value="${ext.dev}/easymock.jar" />
+    <property name="junit"      value="${ext.dev}/junit.jar" />
+    <property name="objenesis"  value="${ext.dev}/objenesis.jar" />
     
+    <property name="testingJars" value="${build}/class:${jars}:${xerces}:${asm}:${cglib}:${easymock}:${junit}:${objenesis}" />
+
 </project>
diff --git a/projects/cadcAccessControl/doc/AccessControl.png b/projects/cadcAccessControl/doc/AccessControl.png
index 52bcea3a9f84a12817a27af6e2e3f7623e5b6b6f..14156bbd6833002185ee0487130121422e2ddd03 100644
Binary files a/projects/cadcAccessControl/doc/AccessControl.png and b/projects/cadcAccessControl/doc/AccessControl.png differ
diff --git a/projects/cadcAccessControl/doc/auth.zargo b/projects/cadcAccessControl/doc/auth.zargo
index 57aa2846f75e01db9d2e032775009c297468d492..8c12b4da6d7e9a1d815ec1a2a2bbb330da46723c 100644
Binary files a/projects/cadcAccessControl/doc/auth.zargo and b/projects/cadcAccessControl/doc/auth.zargo differ
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/AC.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/AC.java
new file mode 100755
index 0000000000000000000000000000000000000000..cacb9a00c2efacc78ee92aa9d23ed35449fe38d9
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/AC.java
@@ -0,0 +1,93 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+/**
+ * Holder of commonly used consts in cadcAccessControl
+ */
+public class AC
+{
+    // Denotes a description given to a group
+    public static final String PROPERTY_GROUP_DESCRIPTION = "ivo://ivoa.net/gms#description";
+    
+    // Denotes the DN of a group owner
+    public static final String PROPERTY_OWNER_DN = "ivo://ivoa.net/gms#owner_dn";
+    
+    // Denotes the DN of a user
+    public static final String PROPERTY_USER_DN = "ivo://ivoa.net/gms#user_dn";
+    
+    // Denotes a group readable by public
+    public static final String PROPERTY_PUBLIC = "ivo://ivoa.net/gms#public";
+    
+    public static final String GMS_SERVICE_URI = "ivo://cadc.nrc.ca/canfargms";
+    
+    // Group URI attribute once the group name is appended
+    public static final String GROUP_URI = "ivo://cadc.nrc.ca/gms#";
+    
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ActivatedGroup.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ActivatedGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..22e445ede8750b77db6dd5f0bc07db03420531db
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ActivatedGroup.java
@@ -0,0 +1,85 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+
+public class ActivatedGroup extends Group
+{
+    public ActivatedGroup(Group group)
+    {
+        super(group.getID(), group.getOwner());
+        this.description = group.description;
+        this.properties = group.getProperties();
+        this.lastModified = group.lastModified;
+        this.getUserMembers().addAll(group.getUserMembers());
+        this.getGroupMembers().addAll(group.getGroupMembers());
+        this.getUserAdmins().addAll(group.getUserAdmins());
+        this.getGroupAdmins().addAll(group.getGroupAdmins());
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
index e89798147a80a826b7c03c819ed51ac35f48748a..c8e8048ee08a45712e47a93380c8441dd42f650a 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Group.java
@@ -1,104 +1,136 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
  */
-
 package ca.nrc.cadc.ac;
 
 import java.security.Principal;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.Set;
 
 public class Group
 {
     private String groupID;
-
+    
     private User<? extends Principal> owner;
-
+    
     // group's properties
     protected Set<GroupProperty> properties = new HashSet<GroupProperty>();
 
     // group's user members
-    private Set<User<? extends Principal>> userMembers = 
-            new HashSet<User<? extends Principal>>();
+    private Set<User<? extends Principal>> userMembers = new HashSet<User<? extends Principal>>();
+
     // group's group members
     private Set<Group> groupMembers = new HashSet<Group>();
-
+    
+    // group's user admins
+    private Set<User<? extends Principal>> userAdmins = new HashSet<User<? extends Principal>>();
+    
+    // group's group admins
+    private Set<Group> groupAdmins = new HashSet<Group>();
+    
     public String description;
+    public Date lastModified;
     
-    // Access Control properties
-    /**
-     * group that can read details of this group
-     * Note: this class does not enforce any access control rules
-     */
-    public Group groupRead;
-    /**
-     * group that can read and write details of this group
-     * Note: this class does not enforce any access control rules
-     */
-    public Group groupWrite;
     /**
-     * flag that show whether the details of this group are publicly readable
-     * Note: this class does not enforce any access control rules
+     * Ctor.
+     * 
+     * @param groupID   Unique ID for the group. Must be a valid URI fragment 
+     *                  component, so it's restricted to alphanumeric 
+     *                  and "-", ".","_","~" characters.
      */
-    public boolean publicRead = false;
+    public Group(String groupID)
+    {
+        this(groupID, null);
+    }
 
     /**
      * Ctor.
      * 
-     * @param groupID
-     *            Unique ID for the group. Must be a valid URI fragment component,
-     *            so it's restricted to alphanumeric and "-", ".","_","~" characters.
-     * @param owner
-     *            Owner/Creator of the group.
+     * @param groupID   Unique ID for the group. Must be a valid URI fragment 
+     *                  component, so it's restricted to alphanumeric 
+     *                  and "-", ".","_","~" characters.
+     * @param owner     Owner/Creator of the group.
      */
-    public Group(final String groupID,
-            final User<? extends Principal> owner)
+    public Group(String groupID, User<? extends Principal> owner)
     {
-        if(groupID == null)
+        if (groupID == null)
         {
             throw new IllegalArgumentException("Null groupID");
         }
-        
-        // check for invalid path characters in groupID
-        if(!groupID.matches("^[a-zA-Z0-9\\-\\.~_]*$"))
-            throw new IllegalArgumentException("Invalid group ID " + groupID
-                    + ": may not contain space ( ), slash (/), escape (\\), or percent (%)");
 
-        this.groupID = groupID;
-        if(owner == null)
+        if (!groupID.matches("^[a-zA-Z0-9\\-\\.~_]*$"))
         {
-            throw new IllegalArgumentException("Null owner");
+            throw new IllegalArgumentException("Invalid group ID " + groupID +
+                    ": may not contain space ( ), slash (/), escape (\\), or percent (%)");
         }
+
+        this.groupID = groupID;
         this.owner = owner;
     }
 
@@ -147,7 +179,24 @@ public class Group
     {
         return groupMembers;
     }
+    
+    /**
+     * 
+     * @return individual user admins of this group
+     */
+    public Set<User<? extends Principal>> getUserAdmins()
+    {
+        return userAdmins;
+    }
 
+    /**
+     * 
+     * @return group admins of this group
+     */
+    public Set<Group> getGroupAdmins()
+    {
+        return groupAdmins;
+    }
 
     /* (non-Javadoc)
      * @see java.lang.Object#hashCode()
@@ -155,7 +204,7 @@ public class Group
     @Override
     public int hashCode()
     {
-        return 31  + groupID.hashCode();
+        return 31 + groupID.toLowerCase().hashCode();
     }
 
     /* (non-Javadoc)
@@ -177,17 +226,16 @@ public class Group
             return false;
         }
         Group other = (Group) obj;
-        if (!groupID.equals(other.groupID))
+        if (!groupID.equalsIgnoreCase(other.groupID))
         {
             return false;
         }
         return true;
     }
-    
+
     @Override
     public String toString()
     {
         return getClass().getSimpleName() + "[" + groupID + "]";
     }
-
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupAlreadyExistsException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupAlreadyExistsException.java
new file mode 100755
index 0000000000000000000000000000000000000000..7e3e096185d78d55b19b0730f4bec3be27feb56c
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupAlreadyExistsException.java
@@ -0,0 +1,81 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+public class GroupAlreadyExistsException extends Exception
+{
+    /**
+     * Thrown when there is a group conflict.
+     *
+     */
+    public GroupAlreadyExistsException(String message)
+    {
+        super(message);
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupNotFoundException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupNotFoundException.java
new file mode 100755
index 0000000000000000000000000000000000000000..a03e53649d4cbda5052a8a893ef76593cc917af5
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupNotFoundException.java
@@ -0,0 +1,81 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+public class GroupNotFoundException extends Exception
+{
+    /**
+     * Thrown when a group cannot be found.
+     *
+     */
+    public GroupNotFoundException(String message)
+    {
+        super(message);
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupProperty.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupProperty.java
index 5ddfa8016e0e75077bf34bac7b7b02b32291d4bc..2c4c89aad4ddf08e1e14fcd59f39c3002f20f1ae 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupProperty.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupProperty.java
@@ -1,71 +1,71 @@
 /*
-************************************************************************
-*******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
-**************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
-*
-*  (c) 2009.                            (c) 2009.
-*  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/>.
-*
-*
-************************************************************************
-*/
-
+ ************************************************************************
+ *******************  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;
 
 /**
@@ -73,7 +73,33 @@ package ca.nrc.cadc.ac;
  *
  */
 public class GroupProperty
-{   
+{
+    /**
+     * Name of the GroupProperty element.
+     */
+    public static final String NAME = "property";
+    
+    /**
+     * Name of the property key attribute in the GroupProperty element.
+     */
+    public static final String KEY_ATTRIBUTE = "key";
+    
+    /**
+     * Name of the property type attribute in the GroupProperty element.
+     */
+    public static final String TYPE_ATTRIBUTE = "type";
+    
+    /**
+     * Name of the property readOnly attribute in the GroupProperty element.
+     */
+    public static final String READONLY_ATTRIBUTE = "readOnly";
+    
+    /**
+     * Allowed types.
+     */
+    public static final String STRING_TYPE = "String";
+    public static final String INTEGER_TYPE = "Integer";
+    
     // The property identifier
     private String key;
     
@@ -82,21 +108,21 @@ public class GroupProperty
     
     // true if the property cannot be modified.
     private boolean readOnly;
-    
 
     /**
      * GroupProperty constructor.
      * 
      * @param key The property key. Cannot be null.
      * @param value The property value.
+     * @param readOnly
      */
     public GroupProperty(String key, Object value, boolean readOnly)
     {
-        if(key == null)
+        if (key == null)
         {
             throw new IllegalArgumentException("Null key");
         }
-        if(value == null)
+        if (value == null)
         {
             throw new IllegalArgumentException("Null value");
         }
@@ -128,9 +154,7 @@ public class GroupProperty
     {
         return readOnly;
     }
-    
 
-    
     /* (non-Javadoc)
      * @see java.lang.Object#hashCode()
      */
@@ -139,7 +163,7 @@ public class GroupProperty
     {
         final int prime = 31;
         int result = 1;
-        result = prime * result + ((key == null) ? 0 : key.hashCode());
+        result = prime * result + (key == null ? 0 : key.hashCode());
         return result;
     }
 
@@ -164,11 +188,10 @@ public class GroupProperty
         GroupProperty other = (GroupProperty) obj;
         return key.equals(other.key);
     }
-    
+
     @Override
     public String toString()
     {
         return getClass().getSimpleName() + "[" + key + ": " + value + "]";
     }
-
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupReader.java
new file mode 100755
index 0000000000000000000000000000000000000000..70ef1830f10432669f5b0bbd27be53286fa8d2b4
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupReader.java
@@ -0,0 +1,302 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.List;
+
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
+import ca.nrc.cadc.date.DateUtil;
+import ca.nrc.cadc.xml.XmlUtil;
+
+public class GroupReader
+{
+
+    /**
+     * Construct a Group from an XML String source.
+     * 
+     * @param xml String of the XML.
+     * @return Group Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static Group read(String xml)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (xml == null)
+        {
+            throw new IllegalArgumentException("XML must not be null");
+        }
+        return read(new StringReader(xml));
+    }
+
+    /**
+     * Construct a Group from a InputStream.
+     * 
+     * @param in InputStream.
+     * @return Group Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static Group read(InputStream in)
+        throws ReaderException, IOException
+    {
+        if (in == null)
+        {
+            throw new IOException("stream closed");
+        }
+        InputStreamReader reader;
+        try
+        {
+            reader = new InputStreamReader(in, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported");
+        }
+        return read(reader);
+    }
+
+    /**
+     * Construct a Group from a Reader.
+     * 
+     * @param reader Reader.
+     * @return Group Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static Group read(Reader reader)
+        throws ReaderException, IOException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Document document;
+        try
+        {
+            document = XmlUtil.buildDocument(reader);
+        }
+        catch (JDOMException jde)
+        {
+            String error = "XML failed validation: " + jde.getMessage();
+            throw new ReaderException(error, jde);
+        }
+
+        Element root = document.getRootElement();
+
+        String groupElemName = root.getName();
+
+        if (!groupElemName.equalsIgnoreCase("group"))
+        {
+            String error = "Expected group element, found " + groupElemName;
+            throw new ReaderException(error);
+        }
+
+        return parseGroup(root);
+    }
+
+    protected static Group parseGroup(Element groupElement)
+        throws ReaderException
+    {
+        String uri = groupElement.getAttributeValue("uri");
+        if (uri == null)
+        {
+            String error = "group missing required uri attribute";
+            throw new ReaderException(error);
+        }
+
+        // Group groupID
+        int index = uri.indexOf(AC.GROUP_URI);
+        if (index == -1)
+        {
+            String error = "group uri attribute malformed: " + uri;
+            throw new ReaderException(error);
+        }
+        String groupID = uri.substring(AC.GROUP_URI.length());
+
+        // Group owner
+        User<? extends Principal> user = null;
+        Element ownerElement = groupElement.getChild("owner");
+        if (ownerElement != null)
+        {
+            // Owner user
+            Element userElement = ownerElement.getChild("user");
+            if (userElement == null)
+            {
+                String error = "owner missing required user element";
+                throw new ReaderException(error);
+            }
+            user = UserReader.parseUser(userElement);
+        }
+
+        Group group = new Group(groupID, user);
+
+        // description
+        Element descriptionElement = groupElement.getChild("description");
+        if (descriptionElement != null)
+        {
+            group.description = descriptionElement.getText();
+        }
+
+        // lastModified
+        Element lastModifiedElement = groupElement.getChild("lastModified");
+        if (lastModifiedElement != null)
+        {
+            try
+            {
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                group.lastModified = df.parse(lastModifiedElement.getText());
+            }
+            catch (ParseException e)
+            {
+                String error = "Unable to parse group lastModified because " + e.getMessage();
+
+                throw new ReaderException(error);
+            }
+
+        }
+        
+        // properties
+        Element propertiesElement = groupElement.getChild("properties");
+        if (propertiesElement != null)
+        {
+            List<Element> propertyElements = propertiesElement.getChildren("property");
+            for (Element propertyElement : propertyElements)
+            {
+                group.getProperties().add(GroupPropertyReader.read(propertyElement));
+            }
+
+        }
+
+        // groupMembers
+        Element groupMembersElement = groupElement.getChild("groupMembers");
+        if (groupMembersElement != null)
+        {
+            List<Element> groupElements = groupMembersElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupMembers().add(parseGroup(groupMember));
+            }
+
+        }
+
+        // userMembers
+        Element userMembersElement = groupElement.getChild("userMembers");
+        if (userMembersElement != null)
+        {
+            List<Element> userElements = userMembersElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserMembers().add(UserReader.parseUser(userMember));
+            }
+        }
+        
+        // groupAdmins
+        Element groupAdminsElement = groupElement.getChild("groupAdmins");
+        if (groupAdminsElement != null)
+        {
+            List<Element> groupElements = groupAdminsElement.getChildren("group");
+            for (Element groupMember : groupElements)
+            {
+                group.getGroupAdmins().add(parseGroup(groupMember));
+            }
+
+        }
+
+        // userAdmins
+        Element userAdminsElement = groupElement.getChild("userAdmins");
+        if (userAdminsElement != null)
+        {
+            List<Element> userElements = userAdminsElement.getChildren("user");
+            for (Element userMember : userElements)
+            {
+                group.getUserAdmins().add(UserReader.parseUser(userMember));
+            }
+        }
+
+        return group;
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupWriter.java
new file mode 100755
index 0000000000000000000000000000000000000000..93fcd13c61f9a5492beb3468049997cbeb908b25
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupWriter.java
@@ -0,0 +1,268 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.security.Principal;
+import java.text.DateFormat;
+
+import org.jdom2.Attribute;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+
+import ca.nrc.cadc.date.DateUtil;
+import ca.nrc.cadc.util.StringBuilderWriter;
+
+public class GroupWriter
+{
+    /**
+     * Write a Group to a StringBuilder.
+     * @param group
+     * @param builder
+     * @throws java.io.IOException
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Group group, StringBuilder builder)
+        throws IOException, WriterException
+    {
+        write(group, new StringBuilderWriter(builder));
+    }
+
+    /**
+     * Write a Group to an OutputStream.
+     * 
+     * @param group Group to write.
+     * @param out OutputStream to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Group group, OutputStream out)
+        throws IOException, WriterException
+    {
+        OutputStreamWriter outWriter;
+        try
+        {
+            outWriter = new OutputStreamWriter(out, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported", e);
+        }
+        write(group, new BufferedWriter(outWriter));
+    }
+
+    /**
+     * Write a Group to a Writer.
+     * 
+     * @param group Group to write.
+     * @param writer  Writer to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Group group, Writer writer)
+        throws IOException, WriterException
+    {
+        if (group == null)
+        {
+            throw new WriterException("null group");
+        }
+
+        write(getGroupElement(group), writer);
+    }
+
+    /**
+     * 
+     * @param group
+     * @return 
+     * @throws ca.nrc.cadc.ac.WriterException 
+     */
+    public static Element getGroupElement(Group group)
+        throws WriterException
+    {
+        return getGroupElement(group, true);
+    }
+
+    public static Element getGroupElement(Group group, boolean deepCopy)
+        throws WriterException
+    {
+        // Create the root group element.
+        Element groupElement = new Element("group");
+        String groupURI = AC.GROUP_URI + group.getID();
+        groupElement.setAttribute(new Attribute("uri", groupURI));
+
+        // Group owner
+        if (group.getOwner() != null)
+        {
+            Element ownerElement = new Element("owner");
+            Element userElement = UserWriter.getUserElement(group.getOwner());
+            ownerElement.addContent(userElement);
+            groupElement.addContent(ownerElement);
+        }
+
+        if (deepCopy)
+        {
+            // Group description
+            if (group.description != null)
+            {
+                Element descriptionElement = new Element("description");
+                descriptionElement.setText(group.description);
+                groupElement.addContent(descriptionElement);
+            }
+
+            // lastModified
+            if (group.lastModified != null)
+            {
+                Element lastModifiedElement = new Element("lastModified");
+                DateFormat df = DateUtil.getDateFormat(DateUtil.IVOA_DATE_FORMAT, DateUtil.UTC);
+                lastModifiedElement.setText(df.format(group.lastModified));
+                groupElement.addContent(lastModifiedElement);
+            }
+
+            // Group properties
+            if (!group.getProperties().isEmpty())
+            {
+                Element propertiesElement = new Element("properties");
+                for (GroupProperty property : group.getProperties())
+                {
+                    propertiesElement.addContent(GroupPropertyWriter.write(property));
+                }
+                groupElement.addContent(propertiesElement);
+            }
+
+            // Group groupMembers.
+            if ((group.getGroupMembers() != null) && (!group.getGroupMembers().isEmpty()))
+            {
+                Element groupMembersElement = new Element("groupMembers");
+                for (Group groupMember : group.getGroupMembers())
+                {
+                    groupMembersElement.addContent(getGroupElement(groupMember, false));
+                }
+                groupElement.addContent(groupMembersElement);
+            }
+
+            // Group userMembers
+            if ((group.getUserMembers() != null) && (!group.getUserMembers().isEmpty()))
+            {
+                Element userMembersElement = new Element("userMembers");
+                for (User<? extends Principal> userMember : group.getUserMembers())
+                {
+                    userMembersElement.addContent(UserWriter.getUserElement(userMember));
+                }
+                groupElement.addContent(userMembersElement);
+            }
+            
+            // Group groupAdmins.
+            if ((group.getGroupAdmins() != null) && (!group.getGroupAdmins().isEmpty()))
+            {
+                Element groupAdminsElement = new Element("groupAdmins");
+                for (Group groupMember : group.getGroupAdmins())
+                {
+                    groupAdminsElement.addContent(getGroupElement(groupMember, false));
+                }
+                groupElement.addContent(groupAdminsElement);
+            }
+
+            // Group userAdmins
+            if ((group.getUserAdmins() != null) && (!group.getUserAdmins().isEmpty()))
+            {
+                Element userAdminsElement = new Element("userAdmins");
+                for (User<? extends Principal> userMember : group.getUserAdmins())
+                {
+                    userAdminsElement.addContent(UserWriter.getUserElement(userMember));
+                }
+                groupElement.addContent(userAdminsElement);
+            }
+        }
+
+        return groupElement;
+    }
+
+    /**
+     * Write to root Element to a writer.
+     * 
+     * @param root Root Element to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     */
+    private static void write(Element root, Writer writer)
+        throws IOException
+    {
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        outputter.output(new Document(root), writer);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsReader.java
new file mode 100755
index 0000000000000000000000000000000000000000..656bdeccb031fb47872caa553a41ee7808909845
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsReader.java
@@ -0,0 +1,188 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.xml.XmlUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
+public class GroupsReader
+{
+    /**
+     * Construct a list of Group's from an XML String source.
+     * 
+     * @param xml String of the XML.
+     * @return Groups List of Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static List<Group> read(String xml)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (xml == null)
+        {
+            throw new IllegalArgumentException("XML must not be null");
+        }
+        return read(new StringReader(xml));
+    }
+
+    /**
+     * Construct a list of Group's from a InputStream.
+     * 
+     * @param in InputStream.
+     * @return Groups List of Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static List<Group> read(InputStream in)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (in == null)
+        {
+            throw new IOException("stream closed");
+        }
+        InputStreamReader reader;
+        try
+        {
+            reader = new InputStreamReader(in, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported");
+        }
+        return read(reader);
+    }
+
+    /**
+     * Construct a List of Group's from a Reader.
+     * 
+     * @param reader Reader.
+     * @return Groups List of Group.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static List<Group> read(Reader reader)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        Document document;
+        try
+        {
+            document = XmlUtil.buildDocument(reader);
+        }
+        catch (JDOMException jde)
+        {
+            String error = "XML failed validation: " + jde.getMessage();
+            throw new ReaderException(error, jde);
+        }
+
+        Element root = document.getRootElement();
+
+        String groupElemName = root.getName();
+
+        if (!groupElemName.equalsIgnoreCase("groups"))
+        {
+            String error = "Expected groups element, found " + groupElemName;
+            throw new ReaderException(error);
+        }
+
+        return parseGroups(root);
+    }
+
+    protected static List<Group> parseGroups(Element groupsElement)
+            throws URISyntaxException, ReaderException
+    {
+        List<Group> groups = new ArrayList<Group>();
+
+        List<Element> groupElements = groupsElement.getChildren("group");
+        for (Element groupElement : groupElements)
+        {
+            groups.add(GroupReader.parseGroup(groupElement));
+        }
+
+        return groups;
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsWriter.java
new file mode 100755
index 0000000000000000000000000000000000000000..0bdcf1f09ebeb40da3610f6a5ce69be834f86540
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/GroupsWriter.java
@@ -0,0 +1,107 @@
+package ca.nrc.cadc.ac;
+
+import ca.nrc.cadc.util.StringBuilderWriter;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.util.Collection;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+
+public class GroupsWriter
+{
+    /**
+     * Write a List of Group's to a StringBuilder.
+     * @param groups List of Group's to write.
+     * @param builder
+     * @throws java.io.IOException
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Collection<Group> groups, StringBuilder builder)
+        throws IOException, WriterException
+    {
+        write(groups, new StringBuilderWriter(builder));
+    }
+
+    /**
+     * Write a List of Group's to an OutputStream.
+     * 
+     * @param groups List of Group's to write.
+     * @param out OutputStream to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Collection<Group> groups, OutputStream out)
+        throws IOException, WriterException
+    {
+        OutputStreamWriter outWriter;
+        try
+        {
+        outWriter = new OutputStreamWriter(out, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+        throw new RuntimeException("UTF-8 encoding not supported", e);
+        }
+        write(groups, new BufferedWriter(outWriter));
+    }
+
+    /**
+     * Write a List of Group's to a Writer.
+     * 
+     * @param groups List of Group's to write.
+     * @param writer  Writer to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(Collection<Group> groups, Writer writer)
+        throws IOException, WriterException
+    {
+        if (groups == null)
+        {
+        throw new WriterException("null groups");
+        }
+
+        write(getGroupsElement(groups), writer);
+    }
+
+    /**
+     * 
+     * @param groups List of Group's to write.
+     * @return Element of list of Group's.
+     * @throws ca.nrc.cadc.ac.WriterException 
+     */
+    public static Element getGroupsElement(Collection<Group> groups)
+        throws WriterException
+    {
+        Element groupsElement = new Element("groups");
+
+        for (Group group : groups)
+        {
+            groupsElement.addContent(GroupWriter.getGroupElement(group));
+        }
+
+        return groupsElement;
+    }
+
+    /**
+     * Write to root Element to a writer.
+     * 
+     * @param root Root Element to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     */
+    private static void write(Element root, Writer writer)
+        throws IOException
+    {
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        outputter.output(new Document(root), writer);
+    }
+    
+}
\ No newline at end of file
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberAlreadyExistsException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberAlreadyExistsException.java
new file mode 100755
index 0000000000000000000000000000000000000000..1dea9e6e8d70a0aceeb361c0b8bb3dbd4be8c0bb
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberAlreadyExistsException.java
@@ -0,0 +1,86 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+/**
+ * Thrown when there is a member conflict.
+ *
+ */
+public class MemberAlreadyExistsException extends Exception
+{
+    public MemberAlreadyExistsException()
+    {
+        super();
+    }
+    
+    public MemberAlreadyExistsException(String message)
+    {
+        super(message);
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberNotFoundException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberNotFoundException.java
new file mode 100755
index 0000000000000000000000000000000000000000..854bba82089b19a51ee6315d8f827a145851f4b4
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/MemberNotFoundException.java
@@ -0,0 +1,86 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+/**
+ * Thrown when a member could not be found.
+ *
+ */
+public class MemberNotFoundException extends Exception
+{
+    public MemberNotFoundException()
+    {
+        super();
+    }
+    
+    public MemberNotFoundException(String message)
+    {
+        super(message);
+    }
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PersonalDetails.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PersonalDetails.java
index a5ca2405f39ea169147858ebeba054c9963e08eb..60d9a81348e5eb65a9d6563c28703376d713af78 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PersonalDetails.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PersonalDetails.java
@@ -1,51 +1,124 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
  */
-
 package ca.nrc.cadc.ac;
 
 public class PersonalDetails implements UserDetails
 {
+    /**
+     * Name of the PersonalDetails element.
+     */
+    public static final String NAME = "personalDetails";
+    
+    /**
+     * Name of the firstName element.
+     */
+    public static final String FIRSTNAME = "firstName";
+    
+    /**
+     * Name of the lastName element.
+     */
+    public static final String LASTNAME = "lastName";
+    
+    /**
+     * Name of the email element.
+     */
+    public static final String EMAIL = "email";
+    
+    /**
+     * Name of the email element.
+     */
+    public static final String ADDRESS = "address";
+    
+    /**
+     * Name of the email element.
+     */
+    public static final String INSTITUTE = "institute";
+    
+    /**
+     * Name of the email element.
+     */
+    public static final String CITY = "city";
+    
+    /**
+     * Name of the email element.
+     */
+    public static final String COUNTRY = "country";
+        
     private String firstName;
     private String lastName;
-    private String email;
-    private String address;
-    private String institute;
-    private String city;
-    private String country;
+    public String email;
+    public String address;
+    public String institute;
+    public String city;
+    public String country;
 
-    public PersonalDetails(String firstName, String lastName, String email,
-            String address, String institute, String city, String country)
+    public PersonalDetails(String firstName, String lastName)
     {
         if (firstName == null)
         {
@@ -55,34 +128,9 @@ public class PersonalDetails implements UserDetails
         {
             throw new IllegalArgumentException("null lastName");
         }
-        if (email == null)
-        {
-            throw new IllegalArgumentException("null email");
-        }
 
-        if (address == null)
-        {
-            throw new IllegalArgumentException("null address");
-        }
-        if (institute == null)
-        {
-            throw new IllegalArgumentException("null institute");
-        }
-        if (city == null)
-        {
-            throw new IllegalArgumentException("null city");
-        }
-        if (country == null)
-        {
-            throw new IllegalArgumentException("null country");
-        }
         this.firstName = firstName;
         this.lastName = lastName;
-        this.email = email;
-        this.address = address;
-        this.institute = institute;
-        this.city = city;
-        this.country = country;
     }
 
     public String getFirstName()
@@ -95,45 +143,15 @@ public class PersonalDetails implements UserDetails
         return lastName;
     }
 
-    public String getEmail()
-    {
-        return email;
-    }
-
-    public String getAddress()
-    {
-        return address;
-    }
-
-    public String getInstitute()
-    {
-        return institute;
-    }
-
-    public String getCity()
-    {
-        return city;
-    }
-
-    public String getCountry()
-    {
-        return country;
-    }
-
     /* (non-Javadoc)
      * @see ca.nrc.cadc.auth.model.UserDetails#hashCode()
      */
     @Override
     public int hashCode()
     {
-        final int prime = 31;
+        int prime = 31;
         int result = 1;
-        result = prime * result + address.hashCode();
-        result = prime * result + city.hashCode();
-        result = prime * result + country.hashCode();
-        result = prime * result + email.hashCode();
         result = prime * result + firstName.hashCode();
-        result = prime * result + institute.hashCode();
         result = prime * result + lastName.hashCode();
         return result;
     }
@@ -163,31 +181,7 @@ public class PersonalDetails implements UserDetails
         {
             return false;
         }
-        if (!lastName.equals(other.lastName))
-        {
-            return false;
-        }
-        if (!email.equals(other.email))
-        {
-            return false;
-        }
-        if (!institute.equals(other.institute))
-        {
-            return false;
-        }
-        if (!address.equals(other.address))
-        {
-            return false;
-        }
-        if (!city.equals(other.city))
-        {
-            return false;
-        }
-        if (!country.equals(other.country))
-        {
-            return false;
-        }
-        return true;
+        return lastName.equals(other.lastName);
     }
 
     /* (non-Javadoc)
@@ -196,8 +190,9 @@ public class PersonalDetails implements UserDetails
     @Override
     public String toString()
     {
-        return getClass().getSimpleName() + "[" + firstName + ", "
-                + lastName + ", " + email + ", " + address + ", "
-                + institute + ", " + city + ", " + country + "]";
+        return getClass().getSimpleName() + "[" + firstName + ", " + 
+               lastName + ", " + email + ", " + address + ", " + 
+               institute + ", " + city + ", " + country + "]";
     }
+
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PosixDetails.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PosixDetails.java
index 12d8dec1b4560d9d3b68a681dc36793d51c81247..af282d6d006b05657c490ea9944f31e2074adc0a 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PosixDetails.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/PosixDetails.java
@@ -1,37 +1,71 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
  */
-
 package ca.nrc.cadc.ac;
 
 /**
@@ -39,10 +73,30 @@ package ca.nrc.cadc.ac;
  */
 public class PosixDetails implements UserDetails
 {
+    /**
+     * Name of the PosixDetails element.
+     */
+    public static final String NAME = "posixDetails";
+    
+    /**
+     * Name of the uid element.
+     */
+    public static final String UID = "uid";
+    
+    /**
+     * Name of the gid element.
+     */
+    public static final String GID = "gid";
+    
+    /**
+     * Name of the homeDirectory element.
+     */
+    public static final String HOME_DIRECTORY = "homeDirectory";
+        
     private long uid;
     private long gid;
     private String homeDirectory;
-
+    
     /**
      * user login shell
      */
@@ -50,12 +104,9 @@ public class PosixDetails implements UserDetails
 
     /**
      * 
-     * @param uid
-     *            posix uid
-     * @param gid
-     *            posix gid
-     * @param homeDirectory
-     *            home directory
+     * @param uid posix uid
+     * @param gid posix gid
+     * @param homeDirectory home directory
      */
     public PosixDetails(long uid, long gid, String homeDirectory)
     {
@@ -64,8 +115,9 @@ public class PosixDetails implements UserDetails
         if (homeDirectory == null)
         {
             throw new IllegalArgumentException(
-                    "null home directory in POSIX details");
+                "null home directory in POSIX details");
         }
+
         this.homeDirectory = homeDirectory;
     }
 
@@ -101,11 +153,11 @@ public class PosixDetails implements UserDetails
     @Override
     public int hashCode()
     {
-        final int prime = 31;
+        int prime = 31;
         int result = 1;
-        result = prime * result + (int) (gid ^ (gid >>> 32));
+        result = prime * result + (int) (gid ^ gid >>> 32);
         result = prime * result + homeDirectory.hashCode();
-        result = prime * result + (int) (uid ^ (uid >>> 32));
+        result = prime * result + (int) (uid ^ uid >>> 32);
         return result;
     }
 
@@ -145,8 +197,8 @@ public class PosixDetails implements UserDetails
     @Override
     public String toString()
     {
-        return getClass().getSimpleName() + "[" + uid + ", " + gid + ", "
-                + homeDirectory + "]";
+        return getClass().getSimpleName() + "[" + uid + ", " + 
+               gid + ", " + homeDirectory + "]";
     }
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ReaderException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ReaderException.java
new file mode 100755
index 0000000000000000000000000000000000000000..397d1e62113993fc59cee4f79bbf9a62ea27a62e
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/ReaderException.java
@@ -0,0 +1,110 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import java.io.IOException;
+
+/**
+ * Class for all Exceptions that occur during reading.
+ */
+public class ReaderException extends IOException
+{
+    /**
+     * Constructs a new exception with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public ReaderException(String message)
+    {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * <code>cause</code> is <i>not</i> automatically incorporated in
+     * this exception's detail message.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A <tt>null</tt> value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     * @since 1.4
+     */
+    public ReaderException(String message, Throwable cause)
+    {
+        super(message, cause);
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Role.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Role.java
new file mode 100644
index 0000000000000000000000000000000000000000..3223ef4e70001625b2c5521961c6b8117b52eb54
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/Role.java
@@ -0,0 +1,112 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+/**
+ *
+ * @author jburke
+ */
+public enum Role
+{
+    OWNER("owner"),
+    MEMBER("member"),
+    ADMIN("admin");
+    
+    private final String value;
+
+    private Role(String value)
+    {
+        this.value = value;
+    }
+
+    public static Role toValue(String s)
+    {
+        for (Role role : values())
+            if (role.value.equals(s))
+                return role;
+        throw new IllegalArgumentException("invalid value: " + s);
+    }
+
+    public String getValue()
+    { 
+        return value;
+    }
+
+    public int checksum()
+    {
+        return value.hashCode();
+    }
+    
+    @Override
+    public String toString()
+    {
+        return this.getClass().getSimpleName() + "[" + value + "]";
+    }
+    
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
index 3174d0136d7d2fbbc5bdd0a4fa5e758118ae0ded..22f609ad0ee492fc483f24a34a0f3b87c6ae87b3 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/User.java
@@ -1,39 +1,71 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
  */
-
-
-
 package ca.nrc.cadc.ac;
 
 import java.security.Principal;
@@ -43,46 +75,42 @@ import java.util.Set;
 public class User<T extends Principal>
 {
     private T userID;
-
-    private Set<Principal> principals = new HashSet<Principal>();
     
+    private Set<Principal> identities = new HashSet<Principal>();
+
     public Set<UserDetails> details = new HashSet<UserDetails>();
-    
-    
+
     public User(final T userID)
     {
-        if(userID == null)
+        if (userID == null)
         {
             throw new IllegalArgumentException("null userID");
         }
         this.userID = userID;
     }
-    
-    
-    public Set<Principal> getPrincipals()
+
+    public Set<Principal> getIdentities()
     {
-        return principals;
+        return identities;
     }
-    
+
     public T getUserID()
     {
         return userID;
     }
 
-
     /* (non-Javadoc)
      * @see java.lang.Object#hashCode()
      */
     @Override
     public int hashCode()
     {
-        final int prime = 31;
+        int prime = 31;
         int result = 1;
         result = prime * result + userID.hashCode();
         return result;
     }
 
-
     /* (non-Javadoc)
      * @see java.lang.Object#equals(java.lang.Object)
      */
@@ -101,18 +129,36 @@ public class User<T extends Principal>
         {
             return false;
         }
-        User<?> other = (User<?>) obj;
+        User other = (User) obj;
         if (!userID.equals(other.userID))
         {
             return false;
         }
         return true;
     }
-    
+
     @Override
     public String toString()
     {
         return getClass().getSimpleName() + "[" + userID.getName() + "]";
     }
-    
+
+    public <S extends UserDetails> Set<S> getDetails(
+            final Class<S> userDetailsClass)
+    {
+        final Set<S> matchedDetails = new HashSet<S>();
+
+        for (final UserDetails ud : details)
+        {
+            if (ud.getClass() == userDetailsClass)
+            {
+                // This casting shouldn't happen, but it's the only way to
+                // do this without a lot of work.
+                // jenkinsd 2014.09.26
+                matchedDetails.add((S) ud);
+            }
+        }
+
+        return matchedDetails;
+    }
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserDetails.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserDetails.java
index d03035c1be2377d4de5ebd9174729138006da364..92c259f181c9108f7ba6d56a5772022e112e7eee 100644
--- a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserDetails.java
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserDetails.java
@@ -1,56 +1,99 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
- *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
- *
- * NRC disclaims any warranties         Le CNRC denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
- *
- *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  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;
 
-public interface UserDetails
+public abstract interface UserDetails
 {
-
+    /**
+     * Name of the UserDetails element.
+     */
+    public static final String NAME = "userDetails";
+    
+    /**
+     * Name of the property type attribute in the UserDetails element.
+     */
+    public static final String TYPE_ATTRIBUTE = "type";
+    
     /*
      * (non-Javadoc)
      * 
      * @see java.lang.Object#hashCode()
      */
-    public int hashCode();
+    public abstract int hashCode();
 
     /*
      * (non-Javadoc)
      * 
      * @see java.lang.Object#equals(java.lang.Object)
      */
-    public boolean equals(Object obj);
+    public abstract boolean equals(Object paramObject);
 
-    public String toString();
+    public abstract String toString();
 
 }
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserNotFoundException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserNotFoundException.java
new file mode 100755
index 0000000000000000000000000000000000000000..9b9123c7aeb7c0a244b96767124fc1fc8416c9fc
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserNotFoundException.java
@@ -0,0 +1,82 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+/**
+ * Thrown when a user could not be found.
+ *
+ */
+public class UserNotFoundException extends Exception
+{
+    public UserNotFoundException(String message)
+    {
+        super(message);
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserReader.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserReader.java
new file mode 100755
index 0000000000000000000000000000000000000000..a28883a34a6830c3ad1e4969284e09a6497d2615
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserReader.java
@@ -0,0 +1,217 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.xml.XmlUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.List;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.JDOMException;
+
+public class UserReader
+{
+    /**
+     * Construct a User from an XML String source.
+     * 
+     * @param xml String of the XML.
+     * @return User User.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static User<? extends Principal> read(String xml)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (xml == null)
+        {
+            throw new IllegalArgumentException("XML must not be null");
+        }
+        return read(new StringReader(xml));
+    }
+
+    /**
+     * Construct a User from a InputStream.
+     * 
+     * @param in InputStream.
+     * @return User User.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     * @throws java.net.URISyntaxException
+     */
+    public static User<? extends Principal> read(InputStream in)
+        throws ReaderException, IOException, URISyntaxException
+    {
+        if (in == null)
+        {
+            throw new IOException("stream closed");
+        }
+        InputStreamReader reader;
+        try
+        {
+            reader = new InputStreamReader(in, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported");
+        }
+        return read(reader);
+    }
+
+    /**
+     * Construct a User from a Reader.
+     * 
+     * @param reader Reader.
+     * @return User User.
+     * @throws ca.nrc.cadc.ac.ReaderException
+     * @throws java.io.IOException
+     */
+    public static User<? extends Principal> read(Reader reader)
+        throws ReaderException, IOException
+    {
+        if (reader == null)
+        {
+            throw new IllegalArgumentException("reader must not be null");
+        }
+
+        // Create a JDOM Document from the XML
+        Document document;
+        try
+        {
+            document = XmlUtil.buildDocument(reader);
+        }
+        catch (JDOMException jde)
+        {
+            String error = "XML failed validation: " + jde.getMessage();
+            throw new ReaderException(error, jde);
+        }
+
+        // Root element and namespace of the Document
+        Element root = document.getRootElement();
+
+        return parseUser(root);
+    }
+
+    protected static User<? extends Principal> parseUser(Element userElement)
+        throws ReaderException
+    {
+        // userID element of the User element
+        Element userIDElement = userElement.getChild("userID");
+        if (userIDElement == null)
+        {
+            String error = "userID element not found in user element";
+            throw new ReaderException(error);
+        }
+
+        // identity element of the userID element
+        Element userIDIdentityElement = userIDElement.getChild("identity");
+        if (userIDIdentityElement == null)
+        {
+            String error = "identity element not found in userID element";
+            throw new ReaderException(error);
+        }
+
+        Principal userID = IdentityReader.read(userIDIdentityElement);
+
+        User<Principal> user = new User<Principal>(userID);
+
+        // identities
+        Element identitiesElement = userElement.getChild("identities");
+        if (identitiesElement != null)
+        {
+            List<Element> identityElements = identitiesElement.getChildren("identity");
+            for (Element identityElement : identityElements)
+            {
+                user.getIdentities().add(IdentityReader.read(identityElement));
+            }
+
+        }
+
+        // details
+        Element detailsElement = userElement.getChild("details");
+        if (detailsElement != null)
+        {
+            List<Element> userDetailsElements = detailsElement.getChildren("userDetails");
+            for (Element userDetailsElement : userDetailsElements)
+            {
+                user.details.add(UserDetailsReader.read(userDetailsElement));
+            }
+        }
+
+        return user;
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserWriter.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserWriter.java
new file mode 100755
index 0000000000000000000000000000000000000000..832e41a612a9ed88cb1c5fff6cc5af33753c4275
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/UserWriter.java
@@ -0,0 +1,203 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.util.StringBuilderWriter;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.security.Principal;
+import java.util.Set;
+import org.jdom2.Document;
+import org.jdom2.Element;
+import org.jdom2.output.Format;
+import org.jdom2.output.XMLOutputter;
+
+public class UserWriter
+{
+    /**
+     * Write a User to a StringBuilder.
+     * 
+     * @param user User to write.
+     * @param builder StringBuilder to write to.
+     * @throws java.io.IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(User<? extends Principal> user, StringBuilder builder)
+        throws IOException, WriterException
+    {
+        write(user, new StringBuilderWriter(builder));
+    }
+
+    /**
+     * Write a User to an OutputStream.
+     *
+     * @param user User to write.
+     * @param out OutputStream to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(User<? extends Principal> user, OutputStream out)
+        throws IOException, WriterException
+    {                
+        OutputStreamWriter outWriter;
+        try
+        {
+            outWriter = new OutputStreamWriter(out, "UTF-8");
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw new RuntimeException("UTF-8 encoding not supported", e);
+        }
+        write(user, new BufferedWriter(outWriter));
+    }
+
+    /**
+     * Write a User to a Writer.
+     *
+     * @param user User to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static void write(User<? extends Principal> user, Writer writer)
+        throws IOException, WriterException
+    {
+        if (user == null)
+        {
+            throw new WriterException("null User");
+        }
+        
+        write(getUserElement(user), writer);
+    }
+
+    /**
+     * Build the member Element of a User.
+     *
+     * @param user User.
+     * @return member Element.
+     * @throws ca.nrc.cadc.ac.WriterException
+     */
+    public static Element getUserElement(User<? extends Principal> user)
+        throws WriterException
+    {
+        // Create the user Element.
+        Element userElement = new Element("user");
+
+        // userID element
+        Element userIDElement = new Element("userID");
+        userIDElement.addContent(IdentityWriter.write(user.getUserID()));
+        userElement.addContent(userIDElement);
+
+        // identities
+        Set<Principal> identities = user.getIdentities();
+        if (!identities.isEmpty())
+        {
+            Element identitiesElement = new Element("identities");
+            for (Principal identity : identities)
+            {
+                identitiesElement.addContent(IdentityWriter.write(identity));
+            }
+            userElement.addContent(identitiesElement);
+        }
+
+        // details
+        if (!user.details.isEmpty())
+        {
+            Element detailsElement = new Element("details");
+            Set<UserDetails> userDetails = user.details;
+            for (UserDetails userDetail : userDetails)
+            {
+                detailsElement.addContent(UserDetailsWriter.write(userDetail));
+            }
+            userElement.addContent(detailsElement);
+        }
+
+        return userElement;
+    }
+
+    /**
+     * Write to root Element to a writer.
+     *
+     * @param root Root Element to write.
+     * @param writer Writer to write to.
+     * @throws IOException if the writer fails to write.
+     */
+    private static void write(Element root, Writer writer)
+        throws IOException
+    {
+        XMLOutputter outputter = new XMLOutputter();
+        outputter.setFormat(Format.getPrettyFormat());
+        outputter.output(new Document(root), writer);
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/WriterException.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/WriterException.java
new file mode 100755
index 0000000000000000000000000000000000000000..3bf5a2b87b05ce9cb0c2279cbf7ad72c466801d1
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/WriterException.java
@@ -0,0 +1,110 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import java.io.IOException;
+
+/**
+ * Base exception for all Writer class exceptions.
+ */
+public class WriterException extends IOException
+{
+    /**
+     * Constructs a new exception with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public WriterException(String message)
+    {
+        super(message);
+    }
+
+    /**
+     * Constructs a new exception with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * <code>cause</code> is <i>not</i> automatically incorporated in
+     * this exception's detail message.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A <tt>null</tt> value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     * @since 1.4
+     */
+    public WriterException(String message, Throwable cause)
+    {
+        super(message, cause);
+    }
+
+}
diff --git a/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
new file mode 100755
index 0000000000000000000000000000000000000000..7609ee7326133d0d2975059fdd7666c0e3435f0c
--- /dev/null
+++ b/projects/cadcAccessControl/src/ca/nrc/cadc/ac/client/GMSClient.java
@@ -0,0 +1,1112 @@
+/*
+ ************************************************************************
+ *******************  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 java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.security.AccessControlContext;
+import java.security.AccessControlException;
+import java.security.AccessController;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+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.GroupReader;
+import ca.nrc.cadc.ac.GroupWriter;
+import ca.nrc.cadc.ac.GroupsReader;
+import ca.nrc.cadc.ac.Role;
+import ca.nrc.cadc.ac.UserNotFoundException;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.SSLUtil;
+import ca.nrc.cadc.net.HttpDownload;
+import ca.nrc.cadc.net.HttpPost;
+import ca.nrc.cadc.net.HttpUpload;
+import ca.nrc.cadc.net.InputStreamWrapper;
+import ca.nrc.cadc.net.NetUtil;
+
+
+/**
+ * Client class for performing group searching and group actions
+ * with the access control web service.
+ */
+public class GMSClient
+{
+    private static final Logger log = Logger.getLogger(GMSClient.class);
+    
+    // socket factory to use when connecting
+    private SSLSocketFactory sslSocketFactory;
+    private SSLSocketFactory mySocketFactory;
+    
+    private String baseURL;
+
+    /**
+     * Constructor.
+     * 
+     * @param baseURL The URL of the supporting access control web service
+     * obtained from the registry.
+     */
+    public GMSClient(String baseURL)
+        throws IllegalArgumentException
+    {
+        if (baseURL == null)
+        {
+            throw new IllegalArgumentException("baseURL is required");
+        }
+        try
+        {
+            new URL(baseURL);
+        }
+        catch (MalformedURLException e)
+        {
+            throw new IllegalArgumentException("URL is malformed: " + 
+                                               e.getMessage());
+        }
+
+        if (baseURL.endsWith("/"))
+        {
+            this.baseURL = baseURL.substring(0, baseURL.length() - 1);
+        }
+        else
+        {
+            this.baseURL = baseURL;
+        }
+    }
+
+    /**
+     * Get a list of groups.
+     *
+     * @return The list of groups.
+     */
+    public List<Group> getGroups()
+    {
+        throw new UnsupportedOperationException("Not yet implemented");
+    }
+
+    /**
+     * Create a new group.
+     *
+     * @param group The group to create
+     * @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 UserNotFoundException
+     * @throws IOException
+     */
+    public Group createGroup(Group group)
+        throws GroupAlreadyExistsException, AccessControlException, 
+               UserNotFoundException, IOException
+    {
+        URL createGroupURL = new URL(this.baseURL + "/groups");
+        log.debug("createGroupURL request to " + createGroupURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        StringBuilder groupXML = new StringBuilder();
+        GroupWriter.write(group, groupXML);
+        log.debug("createGroup: " + groupXML);
+
+        byte[] bytes = groupXML.toString().getBytes("UTF-8");
+        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
+
+        HttpUpload transfer = new HttpUpload(in, createGroupURL);
+        transfer.setSSLSocketFactory(getSSLSocketFactory());
+
+        transfer.run();
+
+        Throwable error = transfer.getThrowable();
+        if (error != null)
+        {
+            log.debug("createGroup throwable", error);
+            // transfer returns a -1 code for anonymous uploads.
+            if ((transfer.getResponseCode() == -1) || 
+                (transfer.getResponseCode() == 401) || 
+                (transfer.getResponseCode() == 403))
+            {
+                throw new AccessControlException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 400)
+            {
+                throw new IllegalArgumentException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 409)
+            {
+                throw new GroupAlreadyExistsException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 404)
+            {
+                throw new UserNotFoundException(error.getMessage());
+            }
+            throw new IOException(error);
+        }
+
+        String retXML = transfer.getResponseBody();
+        try
+        {
+            log.debug("createGroup returned: " + retXML);
+            return GroupReader.read(retXML);
+        }
+        catch (Exception bug)
+        {
+            log.error("Unexpected exception", bug);
+            throw new RuntimeException(bug);
+        }
+    }
+
+    /**
+     * Get the group object.
+     *
+     * @param groupName Identifies the group to get.
+     * @return The group.
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws java.io.IOException
+     */
+    public Group getGroup(String groupName)
+        throws GroupNotFoundException, AccessControlException, IOException
+    {
+        URL getGroupURL = new URL(this.baseURL + "/groups/" + groupName);
+        log.debug("getGroup request to " + getGroupURL.toString());
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        HttpDownload transfer = new HttpDownload(getGroupURL, out);
+
+        transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.run();
+
+        Throwable error = transfer.getThrowable();
+        if (error != null)
+        {
+            log.debug("getGroup throwable (" + transfer.getResponseCode() + ")", error);
+            // transfer returns a -1 code for anonymous access.
+            if ((transfer.getResponseCode() == -1) || 
+                (transfer.getResponseCode() == 401) || 
+                (transfer.getResponseCode() == 403))
+            {
+                throw new AccessControlException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 400)
+            {
+                throw new IllegalArgumentException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 404)
+            {
+                throw new GroupNotFoundException(error.getMessage());
+            }
+            throw new IOException(error);
+        }
+
+        try
+        {
+            String groupXML = new String(out.toByteArray(), "UTF-8");
+            log.debug("getGroup returned: " + groupXML);
+            return GroupReader.read(groupXML);
+        }
+        catch (Exception bug)
+        {
+            log.error("Unexpected exception", bug);
+            throw new RuntimeException(bug);
+        }
+    }
+    
+    /**
+     * Get the all group names.
+     *
+     * @return The list of names.
+     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws java.io.IOException
+     */
+    public List<String> getGroupNames()
+        throws AccessControlException, IOException
+    {
+        final URL getGroupNamesURL = new URL(this.baseURL + "/groups");
+        log.debug("getGroupNames request to " + getGroupNamesURL.toString());
+
+        final List<String> groupNames = new ArrayList<String>();
+        final HttpDownload httpDownload =
+                new HttpDownload(getGroupNamesURL, new InputStreamWrapper()
+        {
+            @Override
+            public void read(final InputStream inputStream) throws IOException
+            {
+                try
+                {
+                    InputStreamReader inReader = new InputStreamReader(inputStream);
+                    BufferedReader reader = new BufferedReader(inReader);
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        groupNames.add(line);
+                    }
+                }
+                catch (Exception bug)
+                {
+                    log.error("Unexpected exception", bug);
+                    throw new RuntimeException(bug);
+                }
+            }
+        });
+
+        httpDownload.setSSLSocketFactory(getSSLSocketFactory());
+        httpDownload.run();
+
+        final Throwable error = httpDownload.getThrowable();
+
+        if (error != null)
+        {
+            final String errMessage = error.getMessage();
+            final int responseCode = httpDownload.getResponseCode();
+
+            log.debug("getGroupNames response " + responseCode + ": " +
+                      errMessage);
+
+            if ((responseCode == 401) || (responseCode == 403) || 
+                    (responseCode == -1))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            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 groupNames;
+    }
+
+    /**
+     * Update a group.
+     *
+     * @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 java.io.IOException
+     */
+    public void updateGroup(Group group)
+        throws IllegalArgumentException, GroupNotFoundException, UserNotFoundException,
+               AccessControlException, IOException
+    {
+        URL updateGroupURL = new URL(this.baseURL + "/groups/" + group.getID());
+        log.debug("updateGroup request to " + updateGroupURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        StringBuilder groupXML = new StringBuilder();
+        GroupWriter.write(group, groupXML);
+        log.debug("updateGroup: " + groupXML);
+
+        HttpPost transfer = new HttpPost(updateGroupURL, groupXML.toString(), 
+                                         "application/xml", false);
+
+        transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.run();
+
+        Throwable error = transfer.getThrowable();
+        if (error != null)
+        {
+            // transfer returns a -1 code for anonymous access.
+            if ((transfer.getResponseCode() == -1) || 
+                (transfer.getResponseCode() == 401) || 
+                (transfer.getResponseCode() == 403))
+            {
+                throw new AccessControlException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 400)
+            {
+                throw new IllegalArgumentException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 404)
+            {
+                if (error.getMessage() != null && error.getMessage().toLowerCase().contains("user"))
+                    throw new UserNotFoundException(error.getMessage());
+                else
+                    throw new GroupNotFoundException(error.getMessage());
+            }
+            throw new IOException(error);
+        }
+    }
+
+    /**
+     * Delete the group.
+     *
+     * @param groupName Identifies the group to delete.
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws AccessControlException If unauthorized to perform this operation.
+     * @throws java.io.IOException
+     */
+    public void deleteGroup(String groupName)
+        throws GroupNotFoundException, AccessControlException, IOException
+    {
+        URL deleteGroupURL = new URL(this.baseURL + "/groups/" + groupName);
+        log.debug("deleteGroup request to " + deleteGroupURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        HttpURLConnection conn = 
+                (HttpURLConnection) deleteGroupURL.openConnection();
+        conn.setRequestMethod("DELETE");
+
+        SSLSocketFactory sf = getSSLSocketFactory();
+        if ((sf != null) && ((conn instanceof HttpsURLConnection)))
+        {
+            ((HttpsURLConnection) conn)
+                    .setSSLSocketFactory(sf);
+        }
+
+        final int responseCode;
+
+        try
+        {
+            responseCode = conn.getResponseCode();
+        }
+        catch(Exception e)
+        {
+            throw new AccessControlException(e.getMessage());
+        }
+        
+        if (responseCode != 200)
+        {
+            String errMessage = NetUtil.getErrorBody(conn);
+            log.debug("deleteGroup response " + responseCode + ": " + 
+                      errMessage);
+
+            if ((responseCode == 401) || (responseCode == 403) || 
+                    (responseCode == -1))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+            if (responseCode == 404)
+            {
+                throw new GroupNotFoundException(errMessage);
+            }
+            throw new IOException("HttpResponse (" + responseCode + ") - " + errMessage);
+        }
+    }
+
+    /**
+     * Add a group as a member of another group.
+     *
+     * @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 java.io.IOException
+     */
+    public void addGroupMember(String targetGroupName, String groupMemberName)
+        throws IllegalArgumentException, GroupNotFoundException,
+               AccessControlException, IOException
+    {
+        URL addGroupMemberURL = new URL(this.baseURL + "/groups/" + 
+                                        targetGroupName + "/groupMembers/" + 
+                                        groupMemberName);
+        log.debug("addGroupMember request to " + addGroupMemberURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        final InputStream is = new ByteArrayInputStream(new byte[0]);
+        final HttpUpload httpUpload = new HttpUpload(is, addGroupMemberURL);
+
+        httpUpload.setSSLSocketFactory(getSSLSocketFactory());
+        httpUpload.run();
+
+        final Throwable error = httpUpload.getThrowable();
+        if (error != null)
+        {
+            final int responseCode = httpUpload.getResponseCode();
+            final String errMessage = error.getMessage();
+
+            if ((responseCode == -1) || 
+                (responseCode == 401) || 
+                (responseCode == 403))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+            if (responseCode == 404)
+            {
+                throw new GroupNotFoundException(errMessage);
+            }
+            throw new IOException(errMessage);
+        }
+    }
+
+    /**
+     * 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.
+     * @throws GroupNotFoundException If the group 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
+    {
+        String userIDType = AuthenticationUtil.getPrincipalType(userID);
+        String encodedUserID = URLEncoder.encode(userID.getName(), "UTF-8");
+        URL addUserMemberURL = new URL(this.baseURL + "/groups/" + 
+                                       targetGroupName + "/userMembers/" + 
+                                       encodedUserID + "?idType=" + userIDType);
+
+        log.debug("addUserMember request to " + addUserMemberURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        final InputStream is = new ByteArrayInputStream(new byte[0]);
+        final HttpUpload httpUpload = new HttpUpload(is, addUserMemberURL);
+
+        httpUpload.setSSLSocketFactory(getSSLSocketFactory());
+        httpUpload.run();
+
+        final Throwable error = httpUpload.getThrowable();
+        if (error != null)
+        {
+            final int responseCode = httpUpload.getResponseCode();
+            final String errMessage = error.getMessage();
+
+            if ((responseCode == -1) || 
+                (responseCode == 401) || 
+                (responseCode == 403))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+            if (responseCode == 404)
+            {
+                if (errMessage != null && errMessage.toLowerCase().contains("user"))
+                    throw new UserNotFoundException(errMessage);
+                else
+                    throw new GroupNotFoundException(errMessage);
+            }
+            throw new IOException(errMessage);
+        }
+    }
+
+    /**
+     * Remove a group as a member of another group.
+     *
+     * @param targetGroupName The group from which to remove the group member.
+     * @param groupMemberName The group member to remove.
+     * @throws GroupNotFoundException If the group was not found.
+     * @throws java.io.IOException
+     * @throws AccessControlException If unauthorized to perform this operation.
+     */
+    public void removeGroupMember(String targetGroupName, 
+                                  String groupMemberName)
+        throws GroupNotFoundException, AccessControlException, IOException
+    {
+        URL removeGroupMemberURL = new URL(this.baseURL + "/groups/" + 
+                                           targetGroupName + "/groupMembers/" + 
+                                           groupMemberName);
+        log.debug("removeGroupMember request to " + 
+                  removeGroupMemberURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        HttpURLConnection conn = 
+                (HttpURLConnection) removeGroupMemberURL.openConnection();
+        conn.setRequestMethod("DELETE");
+
+        SSLSocketFactory sf = getSSLSocketFactory();
+        if ((sf != null) && ((conn instanceof HttpsURLConnection)))
+        {
+            ((HttpsURLConnection) conn)
+                    .setSSLSocketFactory(getSSLSocketFactory());
+        }
+        
+        // Try to handle anonymous access and throw AccessControlException 
+        int responseCode = -1;
+        try
+        {
+            responseCode = conn.getResponseCode();
+        }
+        catch (Exception ignore) {}
+        
+        if (responseCode != 200)
+        {
+            String errMessage = NetUtil.getErrorBody(conn);
+            log.debug("removeGroupMember response " + responseCode + ": " + 
+                      errMessage);
+
+            if ((responseCode == -1) || 
+                (responseCode == 401) || 
+                (responseCode == 403))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+            if (responseCode == 404)
+            {
+                throw new GroupNotFoundException(errMessage);
+            }
+            throw new IOException(errMessage);
+        }
+    }
+
+    /**
+     * 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.
+     * @throws GroupNotFoundException If the group 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
+    {
+        String userIDType = AuthenticationUtil.getPrincipalType(userID);
+        String encodedUserID = URLEncoder.encode(userID.toString(), "UTF-8");
+        URL removeUserMemberURL = new URL(this.baseURL + "/groups/" + 
+                                          targetGroupName + "/userMembers/" + 
+                                          encodedUserID + "?idType=" + 
+                                          userIDType);
+
+        log.debug("removeUserMember request to " +
+                  removeUserMemberURL.toString());
+        
+        // reset the state of the cache
+        clearCache();
+
+        HttpURLConnection conn = 
+                (HttpURLConnection) removeUserMemberURL.openConnection();
+        conn.setRequestMethod("DELETE");
+
+        SSLSocketFactory sf = getSSLSocketFactory();
+        if ((sf != null) && ((conn instanceof HttpsURLConnection)))
+        {
+            ((HttpsURLConnection) conn)
+                    .setSSLSocketFactory(getSSLSocketFactory());
+        }
+        
+        // Try to handle anonymous access and throw AccessControlException 
+        int responseCode = -1;
+        try
+        {
+            responseCode = conn.getResponseCode();
+        }
+        catch (Exception ignore) {}
+
+        if (responseCode != 200)
+        {
+            String errMessage = NetUtil.getErrorBody(conn);
+            log.debug("removeUserMember response " + responseCode + ": " + 
+                      errMessage);
+
+            if ((responseCode == -1) || 
+                (responseCode == 401) || 
+                (responseCode == 403))
+            {
+                throw new AccessControlException(errMessage);
+            }
+            if (responseCode == 400)
+            {
+                throw new IllegalArgumentException(errMessage);
+            }
+            if (responseCode == 404)
+            {
+                if (errMessage != null && errMessage.toLowerCase().contains("user"))
+                    throw new UserNotFoundException(errMessage);
+                else
+                    throw new GroupNotFoundException(errMessage);
+            }
+            throw new IOException(errMessage);
+        }
+    }
+
+    /**
+     * Get all the memberships of the user of a certain role.
+     * 
+     * @param userID Identifies the user.
+     * @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 IllegalArgumentException If a parameter is null.
+     * @throws IOException If an unknown error occured.
+     */
+    public List<Group> getMemberships(Principal userID, Role role)
+        throws UserNotFoundException, AccessControlException, IOException
+    {
+        if (userID == null || role == null)
+        {
+            throw new IllegalArgumentException("userID and role are required.");
+        }
+        
+        List<Group> cachedGroups = getCachedGroups(userID, role);
+        if (cachedGroups != null)
+        {
+            return cachedGroups;
+        }
+        
+        String idType = AuthenticationUtil.getPrincipalType(userID);
+        String id = userID.getName();
+        String roleString = role.getValue();
+        
+        StringBuilder searchGroupURL = new StringBuilder(this.baseURL);
+        searchGroupURL.append("/search?");
+        
+        searchGroupURL.append("ID=").append(URLEncoder.encode(id, "UTF-8"));
+        searchGroupURL.append("&IDTYPE=")
+                .append(URLEncoder.encode(idType, "UTF-8"));
+        searchGroupURL.append("&ROLE=")
+                .append(URLEncoder.encode(roleString, "UTF-8"));
+        
+        log.debug("getMemberships request to " + searchGroupURL.toString());
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        URL url = new URL(searchGroupURL.toString());
+        HttpDownload transfer = new HttpDownload(url, out);
+
+        transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.run();
+
+        Throwable error = transfer.getThrowable();
+        if (error != null)
+        {
+            log.debug("getMemberships throwable", error);
+            // transfer returns a -1 code for anonymous access.
+            if ((transfer.getResponseCode() == -1) || 
+                (transfer.getResponseCode() == 401) || 
+                (transfer.getResponseCode() == 403))
+            {
+                throw new AccessControlException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 404)
+            {
+                throw new UserNotFoundException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 400)
+            {
+                throw new IllegalArgumentException(error.getMessage());
+            }
+            throw new IOException(error);
+        }
+
+        try
+        {
+            String groupsXML = new String(out.toByteArray(), "UTF-8");
+            log.debug("getMemberships returned: " + groupsXML);
+            List<Group> groups = GroupsReader.read(groupsXML);
+            setCachedGroups(userID, groups, role);
+            return groups;
+        }
+        catch (Exception bug)
+        {
+            log.error("Unexpected exception", bug);
+            throw new RuntimeException(bug);
+        }
+    }
+    
+    /**
+     * Return the group, specified by paramter groupName, if the user,
+     * identified by userID, is a member of that group.  Return null
+     * otherwise.
+     * 
+     * This call is identical to getMemberShip(userID, groupName, Role.MEMBER)
+     *  
+     * @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 IllegalArgumentException If a parameter is null.
+     * @throws IOException If an unknown error occured.
+     */
+    public Group getMembership(Principal userID, String groupName)
+        throws UserNotFoundException, AccessControlException, IOException
+    {
+        return getMembership(userID, groupName, Role.MEMBER);
+    }
+    
+    /**
+     * Return the group, specified by paramter groupName, if the user,
+     * identified by userID, is a member (of type role) of that group.
+     * Return null otherwise.
+     * 
+     * @param userID Identifies the user.
+     * @param groupName Identifies the group.
+     * @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 IllegalArgumentException If a parameter is null.
+     * @throws IOException If an unknown error occured.
+     */
+    public Group getMembership(Principal userID, String groupName, Role role)
+        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)
+        {
+            int index = cachedGroups.indexOf(new Group(groupName));
+            if (index != -1)
+            {
+                return cachedGroups.get(index);
+            }
+            else
+            {
+                return null;
+            }
+        }
+        
+        String idType = AuthenticationUtil.getPrincipalType(userID);
+        String id = userID.getName();
+        String roleString = role.getValue();
+        
+        StringBuilder searchGroupURL = new StringBuilder(this.baseURL);
+        searchGroupURL.append("/search?");
+
+        searchGroupURL.append("ID=").append(URLEncoder.encode(id, "UTF-8"));
+        searchGroupURL.append("&IDTYPE=")
+                .append(URLEncoder.encode(idType, "UTF-8"));
+        searchGroupURL.append("&ROLE=")
+                .append(URLEncoder.encode(roleString, "UTF-8"));
+        searchGroupURL.append("&GROUPID=")
+                .append(URLEncoder.encode(groupName, "UTF-8"));
+        
+        log.debug("getMembership request to " + searchGroupURL.toString());
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        URL url = new URL(searchGroupURL.toString());
+        HttpDownload transfer = new HttpDownload(url, out);
+
+        transfer.setSSLSocketFactory(getSSLSocketFactory());
+        transfer.run();
+
+        Throwable error = transfer.getThrowable();
+        if (error != null)
+        {
+            log.debug("getMembership throwable", error);
+            // transfer returns a -1 code for anonymous access.
+            if ((transfer.getResponseCode() == -1) || 
+                (transfer.getResponseCode() == 401) || 
+                (transfer.getResponseCode() == 403))
+            {
+                throw new AccessControlException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 404)
+            {
+                throw new UserNotFoundException(error.getMessage());
+            }
+            if (transfer.getResponseCode() == 400)
+            {
+                throw new IllegalArgumentException(error.getMessage());
+            }
+            throw new IOException(error);
+        }
+
+        try
+        {
+            String groupsXML = new String(out.toByteArray(), "UTF-8");
+            log.debug("getMembership returned: " + groupsXML);
+            List<Group> groups = GroupsReader.read(groupsXML);
+            if (groups.size() == 0)
+            {
+                return null;
+            }
+            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);
+            }
+            throw new IllegalStateException(
+                    "Duplicate membership for " + id + " in group " + groupName);
+        }
+        catch (Exception bug)
+        {
+            log.error("Unexpected exception", bug);
+            throw new RuntimeException(bug);
+        }
+    }
+    
+    /**
+     * Check if userID is a member of groupName.
+     * 
+     * This is equivalent to isMember(userID, groupName, Role.MEMBER)
+     * 
+     * @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 IllegalArgumentException If a parameter is null.
+     * @throws IOException If an unknown error occured.
+     */
+    public boolean isMember(Principal userID, String groupName)
+        throws UserNotFoundException, AccessControlException, IOException
+    {
+        return isMember(userID, groupName, Role.MEMBER);
+    }
+    
+    /**
+     * Check if userID is a member (of type role) of groupName.
+     * 
+     * @param userID Identifies the user.
+     * @param groupName Identifies the group.
+     * @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 IllegalArgumentException If a parameter is null.
+     * @throws IOException If an unknown error occured.
+     */
+    public boolean isMember(Principal userID, String groupName, Role role)
+        throws UserNotFoundException, AccessControlException, IOException
+    {
+        Group group = getMembership(userID, groupName, role);
+        return group != null;
+    }
+
+    /**
+     * @param sslSocketFactory the sslSocketFactory to set
+     */
+    public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory)
+    {
+        if (mySocketFactory != null)
+            throw new IllegalStateException("Illegal use of GMSClient: "
+                    + "cannot set SSLSocketFactory after using one created from Subject");
+        this.sslSocketFactory = sslSocketFactory;
+        clearCache();
+    }
+    
+    private int subjectHashCode = 0;
+    private SSLSocketFactory getSSLSocketFactory()
+    {
+        AccessControlContext ac = AccessController.getContext();
+        Subject s = Subject.getSubject(ac);
+        
+        // no real Subject: can only use the one from setSSLSocketFactory
+        if (s == null || s.getPrincipals().isEmpty())
+        {
+            return sslSocketFactory;
+        }
+        
+        // lazy init
+        if (this.mySocketFactory == null)
+        {
+            log.debug("getSSLSocketFactory: " + s);
+            this.mySocketFactory = SSLUtil.getSocketFactory(s);
+            this.subjectHashCode = s.hashCode();
+        }
+        else
+        {
+            int c = s.hashCode();
+            if (c != subjectHashCode)
+                throw new IllegalStateException("Illegal use of " 
+                        + this.getClass().getSimpleName()
+                        + ": subject change not supported for internal SSLSocketFactory");
+        }
+        return this.mySocketFactory;
+    }
+    
+    protected void clearCache()
+    {
+        AccessControlContext acContext = AccessController.getContext();
+        Subject subject = Subject.getSubject(acContext);
+        
+        if (subject != null)
+        {
+            log.debug("Clearing cache");
+            subject.getPrivateCredentials().clear();
+        }
+    }
+
+    protected List<Group> getCachedGroups(Principal userID, Role role)
+    {
+        AccessControlContext acContext = AccessController.getContext();
+        Subject subject = Subject.getSubject(acContext);
+        
+        // only consult cache if the userID is of the calling subject
+        if (userIsSubject(userID, subject))
+        {
+            Set groupCredentialSet = subject.getPrivateCredentials(GroupMemberships.class);
+            if ((groupCredentialSet != null) && 
+                (groupCredentialSet.size() == 1))
+            {
+                Iterator i = groupCredentialSet.iterator();
+                GroupMemberships groupMemberships = ((GroupMemberships) i.next());
+                return groupMemberships.memberships.get(role);
+            }
+        }
+        return null;
+    }
+
+    protected void setCachedGroups(Principal userID, List<Group> groups, Role role)
+    {
+        AccessControlContext acContext = AccessController.getContext();
+        Subject subject = Subject.getSubject(acContext);
+        
+        // only save to cache if the userID is of the calling subject
+        if (userIsSubject(userID, subject))
+        {
+            log.debug("Caching groups for " + userID + ", role " + role);
+            
+            final GroupMemberships groupCredentials;
+            Set groupCredentialSet = subject.getPrivateCredentials(GroupMemberships.class);
+            if ((groupCredentialSet != null) && 
+                (groupCredentialSet.size() == 1))
+            {
+                Iterator i = groupCredentialSet.iterator();
+                groupCredentials = ((GroupMemberships) i.next());
+            }
+            else
+            {
+                groupCredentials = new GroupMemberships();
+                subject.getPrivateCredentials().add(groupCredentials);
+            }
+            
+            groupCredentials.memberships.put(role,  groups);
+        }
+    }
+    
+    protected boolean userIsSubject(Principal userID, Subject subject)
+    {
+        if (userID == null || subject == null)
+        {
+            return false;
+        }
+        
+        for (Principal subjectPrincipal : subject.getPrincipals())
+        {
+            if (subjectPrincipal.equals(userID))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Class used to hold list of groups in which
+     * a user is a member.
+     */
+    protected class GroupMemberships
+    {
+        Map<Role, List<Group>> memberships = new HashMap<Role, List<Group>>();
+
+        protected GroupMemberships()
+        {
+        }
+
+    }
+
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..189352538636b4f9561ec7ab2a472012f8fb6f0e
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyReaderWriterTest.java
@@ -0,0 +1,178 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import org.apache.log4j.Logger;
+import org.jdom2.Element;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class GroupPropertyReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(GroupPropertyReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        Element element = null;
+        try
+        {
+            GroupProperty gp = GroupPropertyReader.read(element);
+            fail("null element should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element("foo");
+        try
+        {
+            GroupProperty gp = GroupPropertyReader.read(element);
+            fail("element not named 'property' should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element("property");
+        try
+        {
+            GroupProperty gp = GroupPropertyReader.read(element);
+            fail("element without 'key' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element.setAttribute("key", "foo");
+        try
+        {
+            GroupProperty gp = GroupPropertyReader.read(element);
+            fail("element without 'type' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element.setAttribute("type", "Double");
+        try
+        {
+            GroupProperty gp = GroupPropertyReader.read(element);
+            fail("Unsupported 'type' should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            Element element = GroupPropertyWriter.write(null);
+            fail("null GroupProperty should throw WriterException");
+        }
+        catch (WriterException e) {}
+         
+        GroupProperty gp = new GroupProperty("key", new Double(1.0), true);
+        try
+        {
+            Element element = GroupPropertyWriter.write(gp);
+            fail("Unsupported GroupProperty type should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+     
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        // String type
+        GroupProperty expected = new GroupProperty("key", "value", true);
+        Element element = GroupPropertyWriter.write(expected);
+        assertNotNull(element);
+         
+        GroupProperty actual = GroupPropertyReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+         
+        // Integer tuype
+        expected = new GroupProperty("key", new Integer(1), false);
+        element = GroupPropertyWriter.write(expected);
+        assertNotNull(element);
+         
+        actual = GroupPropertyReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+    }
+     
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b19171c414d64d3d812cc3c55e3ac6bf4709e2be
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupPropertyTest.java
@@ -0,0 +1,128 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import org.apache.log4j.Logger;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class GroupPropertyTest
+{
+    private static Logger log = Logger.getLogger(GroupPropertyTest.class);
+    
+    @Test
+    public void simpleGroupPropertyTest() throws Exception
+    {
+        GroupProperty gp1 = new GroupProperty("key", "value", false);
+        
+        assertEquals("key", gp1.getKey());
+        assertEquals("value", gp1.getValue());
+        assertEquals(false, gp1.isReadOnly());
+        
+        GroupProperty gp2 = gp1;
+        assertEquals(gp1.hashCode(), gp2.hashCode());
+        assertEquals(gp1, gp2);
+        assertTrue(gp1 == gp2);
+        
+        // test toString
+        System.out.println(gp1);
+    }
+    
+    @Test
+    public void exceptionTests()
+    {
+        boolean thrown = false;
+        try
+        {
+            new GroupProperty(null, "value", true);
+        }
+        catch(IllegalArgumentException e)
+        {
+            thrown = true;
+        }
+        assertTrue(thrown);
+        
+        
+        thrown = false;
+        try
+        {
+            new GroupProperty("key", null, true);
+        }
+        catch(IllegalArgumentException e)
+        {
+            thrown = true;
+        }
+        assertTrue(thrown);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c54f6ee56d0787551e5c3fb7c45a1fad85b67523
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupReaderWriterTest.java
@@ -0,0 +1,188 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.log4j.Logger;
+import org.junit.Test;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ * @author jburke
+ */
+public class GroupReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(GroupReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            Group g = GroupReader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+        
+        try
+        {
+            InputStream in = null;
+            Group g = GroupReader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+        
+        try
+        {
+            Reader r = null;
+            Group g = GroupReader.read(r);
+            fail("null element should throw ReaderException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            GroupWriter.write(null, new StringBuilder());
+            fail("null Group should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+     
+    @Test
+    public void testMinimalReadWrite()
+        throws Exception
+    {
+        Group expected = new Group("groupID", null);
+                
+        StringBuilder xml = new StringBuilder();
+        GroupWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+        
+        Group actual = GroupReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+    
+    @Test
+    public void testMaximalReadWrite()
+        throws Exception
+    {
+        Group expected = new Group("groupID", new User<Principal>(new HttpPrincipal("foo")));
+        expected.description = "description";
+        expected.lastModified = new Date();
+        expected.properties.add(new GroupProperty("key", "value", true));
+        
+        Group groupMember = new Group("member", new User<Principal>(new OpenIdPrincipal("bar")));
+        User<Principal> userMember = new User<Principal>(new HttpPrincipal("baz"));
+        Group groupAdmin = new Group("admin", new User<Principal>(new X500Principal("cn=foo,o=ca")));
+        User<Principal> userAdmin = new User<Principal>(new HttpPrincipal("admin"));
+        
+        expected.getGroupMembers().add(groupMember);
+        expected.getUserMembers().add(userMember);
+        expected.getGroupAdmins().add(groupAdmin);
+        expected.getUserAdmins().add(userAdmin);
+        
+        StringBuilder xml = new StringBuilder();
+        GroupWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+        
+        Group actual = GroupReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+        assertEquals(expected.description, actual.description);
+        assertEquals(expected.lastModified, actual.lastModified);
+        assertEquals(expected.getProperties(), actual.getProperties());
+        assertEquals(expected.getGroupMembers(), actual.getGroupMembers());
+        assertEquals(expected.getUserMembers(), actual.getUserMembers());
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupTest.java
index 3e48c9a0c5cb5a5446d4645d7470db1951d7b37c..6451d53418baf3c5fa1099b97b738c508f27b70b 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupTest.java
@@ -1,44 +1,80 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
  */
-
-
-
 package ca.nrc.cadc.ac;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 import org.apache.log4j.Logger;
 import org.junit.Test;
-import static org.junit.Assert.*;
 
 import ca.nrc.cadc.auth.HttpPrincipal;
 
@@ -49,55 +85,54 @@ public class GroupTest
     @Test
     public void simpleGroupTest() throws Exception
     {
+        Group group1 = new Group("TestGroup");
+        Group group2 = group1;
+        assertEquals(group1.hashCode(), group2.hashCode());
+        assertEquals(group1, group2);
+        assertTrue(group1 == group2);
         
         User<HttpPrincipal> owner = new User<HttpPrincipal>(new HttpPrincipal("owner"));
-        Group group1 = new Group("TestGroup", owner);
+        Group group3 = new Group("TestGroup", owner);
         User<HttpPrincipal> user = new User<HttpPrincipal>(new HttpPrincipal("user"));
         
-        group1.getUserMembers().add(user);
-        assertEquals(1, group1.getUserMembers().size());
+        group3.getUserMembers().add(user);
+        assertEquals(1, group3.getUserMembers().size());
 
-        Group group2 = group1;
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1, group2);
-        assertTrue(group1 == group2);
+        Group group4 = group3;
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3, group4);
+        assertTrue(group3 == group4);
         
-        group2 = new Group("TestGroup", owner);
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group4 = new Group("TestGroup", owner);
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        group2.getUserMembers().add(user);
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group4.getUserMembers().add(user);
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        group1.getGroupMembers().add(group2);
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group3.getGroupMembers().add(group4);
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        group1.description = "Test group";
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group4.getUserAdmins().add(user);
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        // group read and write equality tests     
-        group1.groupRead = group2;
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group3.getGroupAdmins().add(group4);
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        // group write equality tests
-        group1.groupWrite = group2;
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
-
-        group1.publicRead = true;
-        assertEquals(group1.hashCode(), group2.hashCode());
-        assertEquals(group1,group2);
+        group3.description = "Test group";
+        assertEquals(group3.hashCode(), group4.hashCode());
+        assertEquals(group3,group4);
         
-        group2 = new Group("NewTestGroup-._~.", owner);
-        assertFalse(group1.hashCode() == group2.hashCode());
-        assertFalse(group1.equals(group2));
+        group4 = new Group("NewTestGroup-._~.", owner);
+        assertFalse(group3.hashCode() == group4.hashCode());
+        assertFalse(group3.equals(group4));
         
         // test toString
-        System.out.println(group1);
+        System.out.println(group3);
     }
     
     @Test
@@ -119,10 +154,11 @@ public class GroupTest
         try
         {
             new Group("NewTestGroup", null);
+            thrown = true;
         }
         catch(IllegalArgumentException e)
         {
-            thrown = true;
+            fail("Owner can be null");
         }
         assertTrue(thrown);
         
@@ -160,4 +196,5 @@ public class GroupTest
         }
         assertTrue(thrown);
     }
+    
 }
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupsReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a6d595ccca502e3a72dff9b814ea8d60fcf7f0c
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/GroupsReaderWriterTest.java
@@ -0,0 +1,156 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import javax.security.auth.x500.X500Principal;
+import org.apache.log4j.Logger;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import org.junit.Test;
+
+/**
+ *
+ * @author jburke
+ */
+public class GroupsReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(GroupsReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            List<Group> g = GroupsReader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+        
+        try
+        {
+            InputStream in = null;
+            List<Group> g = GroupsReader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+        
+        try
+        {
+            Reader r = null;
+            List<Group> g = GroupsReader.read(r);
+            fail("null element should throw ReaderException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            GroupsWriter.write(null, new StringBuilder());
+            fail("null Group should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+     
+    @Test
+    public void testMinimalReadWrite()
+        throws Exception
+    {        
+        List<Group> expected = new ArrayList<Group>();
+        expected.add(new Group("group1", null));
+        expected.add(new Group("group2", null));
+        
+        StringBuilder xml = new StringBuilder();
+        GroupsWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+        
+        List<Group> actual = GroupsReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected.size(), actual.size());
+        assertEquals(expected.get(0), actual.get(0));
+        assertEquals(expected.get(1), actual.get(1));
+    }
+
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/IdentityReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/IdentityReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..cabd9e945345acd6c2aa10aff8b78d5460980cf5
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/IdentityReaderWriterTest.java
@@ -0,0 +1,192 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import java.security.Principal;
+import javax.management.remote.JMXPrincipal;
+import javax.security.auth.x500.X500Principal;
+import org.apache.log4j.Logger;
+import org.jdom2.Element;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class IdentityReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(IdentityReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        Element element = null;
+        try
+        {
+            Principal p = IdentityReader.read(element);
+            fail("null element should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element("foo");
+        try
+        {
+            Principal p = IdentityReader.read(element);
+            fail("element not named 'identity' should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element("identity");
+        try
+        {
+            Principal p = IdentityReader.read(element);
+            fail("element without 'type' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element.setAttribute("type", "foo");
+        try
+        {
+            Principal p = IdentityReader.read(element);
+            fail("element with unknown 'type' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            Element element = IdentityWriter.write(null);
+            fail("null Identity should throw WriterException");
+        }
+        catch (WriterException e) {}
+         
+        Principal p = new JMXPrincipal("foo");
+        try
+        {
+            Element element = IdentityWriter.write(p);
+            fail("Unsupported Principal type should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+     
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        // X500
+        Principal expected = new X500Principal("cn=foo,o=bar");
+        Element element = IdentityWriter.write(expected);
+        assertNotNull(element);
+         
+        Principal actual = IdentityReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+         
+        // UID
+        expected = new NumericPrincipal(123l);
+        element = IdentityWriter.write(expected);
+        assertNotNull(element);
+         
+        actual = IdentityReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+        
+        // OpenID
+        expected = new OpenIdPrincipal("bar");
+        element = IdentityWriter.write(expected);
+        assertNotNull(element);
+         
+        actual = IdentityReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+        
+        // HTTP
+        expected = new HttpPrincipal("baz");
+        element = IdentityWriter.write(expected);
+        assertNotNull(element);
+         
+        actual = IdentityReader.read(element);
+        assertNotNull(actual);
+         
+        assertEquals(expected, actual);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PersonalDetailsTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PersonalDetailsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..48e99a6f17b87201d88b06e737072e192fd956ac
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PersonalDetailsTest.java
@@ -0,0 +1,130 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import org.apache.log4j.Logger;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class PersonalDetailsTest
+{
+    private static Logger log = Logger.getLogger(PersonalDetailsTest.class);
+    
+    @Test
+    public void simplePersonalDetailsTest() throws Exception
+    {
+        PersonalDetails pd1 = new PersonalDetails("firstname", "lastname");
+        
+        assertEquals("firstname", pd1.getFirstName());
+        assertEquals("lastname", pd1.getLastName());
+
+        PersonalDetails pd2 = pd1;
+        assertEquals(pd1.hashCode(), pd2.hashCode());
+        assertEquals(pd1, pd2);
+        assertTrue(pd1 == pd2);
+        
+        // test toString
+        System.out.println(pd1);
+    }
+    
+    @Test
+    public void exceptionTests()
+    {
+        boolean thrown = false;
+        try
+        {
+            new PersonalDetails(null, "lastname");
+        }
+        catch(IllegalArgumentException e)
+        {
+            thrown = true;
+        }
+        assertTrue(thrown);
+        
+        
+        thrown = false;
+        try
+        {
+            new PersonalDetails("firstname", null);
+        }
+        catch(IllegalArgumentException e)
+        {
+            thrown = true;
+        }
+        assertTrue(thrown);
+    }    
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PosixDetailsTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PosixDetailsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..ba5ecc6a1918edfc8f64d683b6a721e61fd1241b
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/PosixDetailsTest.java
@@ -0,0 +1,116 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import org.apache.log4j.Logger;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class PosixDetailsTest
+{
+    private static Logger log = Logger.getLogger(PosixDetailsTest.class);
+    
+    @Test
+    public void simplePosixDetailsTest() throws Exception
+    {
+        PosixDetails pd1 = new PosixDetails(1l, 2l, "/dev/null");
+        
+        assertEquals(1l, pd1.getUid());
+        assertEquals(2l, pd1.getGid());
+        assertEquals("/dev/null", pd1.getHomeDirectory());
+        
+        PosixDetails pd2 = pd1;
+        assertEquals(pd1.hashCode(), pd2.hashCode());
+        assertEquals(pd1, pd2);
+        assertTrue(pd1 == pd2);
+        
+        // test toString
+        System.out.println(pd1);
+    }
+    
+    @Test
+    public void exceptionTests()
+    {
+        boolean thrown = false;
+        try
+        {
+            new PosixDetails(1l, 2l, null);
+        }
+        catch(IllegalArgumentException e)
+        {
+            thrown = true;
+        }
+        assertTrue(thrown);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/RoleTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/RoleTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..2c6af1400d8ba1dd6d29e52d08496b87dbb33024
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/RoleTest.java
@@ -0,0 +1,153 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.util.Log4jInit;
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class RoleTest
+{
+    private final static Logger log = Logger.getLogger(RoleTest.class);
+    
+    @BeforeClass
+    public static void setUpClass()
+    {
+        Log4jInit.setLevel("ca.nrc.cadc.ac", Level.INFO);
+    }
+    /**
+     * Test of values method, of class Role.
+     */
+    @Test
+    public void testValues()
+    {
+        Role[] expResult = new Role[] { Role.OWNER, Role.MEMBER, Role.ADMIN };
+        Role[] result = Role.values();
+        assertArrayEquals(expResult, result);
+    }
+
+    /**
+     * Test of valueOf method, of class Role.
+     */
+    @Test
+    public void testValueOf()
+    {
+        assertEquals(Role.OWNER, Role.valueOf("OWNER"));
+        assertEquals(Role.MEMBER, Role.valueOf("MEMBER"));
+        assertEquals(Role.ADMIN, Role.valueOf("ADMIN"));
+    }
+
+    /**
+     * Test of toValue method, of class Role.
+     */
+    @Test
+    public void testToValue()
+    {
+        try
+        {
+            Role.toValue("foo");
+            fail("invalid value should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException ignore) {}
+        
+        assertEquals(Role.OWNER, Role.toValue("owner"));
+        assertEquals(Role.MEMBER, Role.toValue("member"));
+        assertEquals(Role.ADMIN, Role.toValue("admin"));
+    }
+
+    /**
+     * Test of getValue method, of class Role.
+     */
+    @Test
+    public void testGetValue()
+    {
+        assertEquals("owner", Role.OWNER.getValue());
+        assertEquals("member", Role.MEMBER.getValue());
+        assertEquals("admin", Role.ADMIN.getValue());
+    }
+
+    /**
+     * Test of checksum method, of class Role.
+     */
+    @Test
+    public void testChecksum()
+    {
+        assertEquals("owner".hashCode(), Role.OWNER.checksum());
+        assertEquals("member".hashCode(), Role.MEMBER.checksum());
+        assertEquals("admin".hashCode(), Role.ADMIN.checksum());
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserDetailsReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserDetailsReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..159333bdda86d33cb8938558c3dabff602ee1ecc
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserDetailsReaderWriterTest.java
@@ -0,0 +1,175 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import ca.nrc.cadc.auth.OpenIdPrincipal;
+import java.security.Principal;
+import javax.management.remote.JMXPrincipal;
+import javax.security.auth.x500.X500Principal;
+import org.apache.log4j.Logger;
+import org.jdom2.Element;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class UserDetailsReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(UserDetailsReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        Element element = null;
+        try
+        {
+            UserDetails ud = UserDetailsReader.read(element);
+            fail("null element should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element("foo");
+        try
+        {
+            UserDetails ud = UserDetailsReader.read(element);
+            fail("element not named 'userDetails' should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element = new Element(UserDetails.NAME);
+        try
+        {
+            UserDetails ud = UserDetailsReader.read(element);
+            fail("element without 'type' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+         
+        element.setAttribute("type", "foo");
+        try
+        {
+            UserDetails ud = UserDetailsReader.read(element);
+            fail("element with unknown 'type' attribute should throw ReaderException");
+        }
+        catch (ReaderException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            Element element = UserDetailsWriter.write(null);
+            fail("null UserDetails should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+     
+    @Test
+    public void testReadWritePersonalDetails()
+        throws Exception
+    {
+        PersonalDetails expected = new PersonalDetails("firstname", "lastname");
+        expected.address = "address";
+        expected.city = "city";
+        expected.country = "country";
+        expected.email = "email";
+        expected.institute = "institute";
+        Element element = UserDetailsWriter.write(expected);
+        assertNotNull(element);
+        
+        PersonalDetails actual = (PersonalDetails) UserDetailsReader.read(element);
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+        assertEquals(expected.address, actual.address);
+        assertEquals(expected.city, actual.city);
+        assertEquals(expected.country, actual.country);
+        assertEquals(expected.email, actual.email);
+        assertEquals(expected.institute, actual.institute);
+    }
+    
+    @Test
+    public void testReadWritePosixDetails()
+        throws Exception
+    {
+        UserDetails expected = new PosixDetails(123l, 456, "/dev/null");
+        Element element = UserDetailsWriter.write(expected);
+        assertNotNull(element);
+        
+        UserDetails actual = UserDetailsReader.read(element);
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserReaderWriterTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserReaderWriterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f4d3e8d31debc38a2add8a522553985260fd552
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserReaderWriterTest.java
@@ -0,0 +1,147 @@
+/*
+ ************************************************************************
+ *******************  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;
+
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.security.Principal;
+import org.apache.log4j.Logger;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ *
+ * @author jburke
+ */
+public class UserReaderWriterTest
+{
+    private static Logger log = Logger.getLogger(UserReaderWriterTest.class);
+
+    @Test
+    public void testReaderExceptions()
+        throws Exception
+    {
+        try
+        {
+            String s = null;
+            User<? extends Principal> u = UserReader.read(s);
+            fail("null String should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+        
+        try
+        {
+            InputStream in = null;
+            User<? extends Principal> u = UserReader.read(in);
+            fail("null InputStream should throw IOException");
+        }
+        catch (IOException e) {}
+        
+        try
+        {
+            Reader r = null;
+            User<? extends Principal> u = UserReader.read(r);
+            fail("null Reader should throw IllegalArgumentException");
+        }
+        catch (IllegalArgumentException e) {}
+    }
+     
+    @Test
+    public void testWriterExceptions()
+        throws Exception
+    {
+        try
+        {
+            UserWriter.write(null, new StringBuilder());
+            fail("null User should throw WriterException");
+        }
+        catch (WriterException e) {}
+    }
+     
+    @Test
+    public void testReadWrite()
+        throws Exception
+    {
+        User<? extends Principal> expected = new User<Principal>(new HttpPrincipal("foo"));
+        expected.getIdentities().add(new NumericPrincipal(123l));
+        expected.details.add(new PersonalDetails("firstname", "lastname"));
+        
+        StringBuilder xml = new StringBuilder();
+        UserWriter.write(expected, xml);
+        assertFalse(xml.toString().isEmpty());
+        
+        User<? extends Principal> actual = UserReader.read(xml.toString());
+        assertNotNull(actual);
+        assertEquals(expected, actual);
+    }
+    
+}
diff --git a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserTest.java b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserTest.java
index 0fce989ccfd154bcb79f29134780d0d486ec9a21..78b636ffbec33f7ca5276c9948b755900b1bca7f 100644
--- a/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserTest.java
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/UserTest.java
@@ -1,42 +1,73 @@
 /*
  ************************************************************************
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
+ *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
+ **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
  *
- * (c) 2014.                            (c) 2014.
- * National Research Council            Conseil national de recherches
- * Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
- * All rights reserved                  Tous droits reserves
+ *  (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 denie toute garantie
- * expressed, implied, or statu-        enoncee, implicite ou legale,
- * tory, of any kind with respect       de quelque nature que se soit,
- * to the software, including           concernant le logiciel, y com-
- * without limitation any war-          pris sans restriction toute
- * ranty of merchantability or          garantie de valeur marchande
- * fitness for a particular pur-        ou de pertinence pour un usage
- * pose.  NRC shall not be liable       particulier.  Le CNRC ne
- * in any event for any damages,        pourra en aucun cas etre tenu
- * whether direct or indirect,          responsable de tout dommage,
- * special or general, consequen-       direct ou indirect, particul-
- * tial or incidental, arising          ier ou general, accessoire ou
- * from the use of the software.        fortuit, resultant de l'utili-
- *                                      sation du logiciel.
+ *  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 $
  *
- * @author adriand
- * 
- * @version $Revision: $
- * 
- * 
- ****  C A N A D I A N   A S T R O N O M Y   D A T A   C E N T R E  *****
  ************************************************************************
- */
+ */package ca.nrc.cadc.ac;
 
-package ca.nrc.cadc.ac;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 import javax.security.auth.x500.X500Principal;
 
@@ -65,8 +96,7 @@ public class UserTest
         assertEquals(user1, user2);
         assertEquals(user1.hashCode(), user2.hashCode());
 
-        user1.details.add(new PersonalDetails("Joe", "Raymond",
-                "jr@email.com", "123 Street", "CADC", "Victoria", "CA"));
+        user1.details.add(new PersonalDetails("Joe", "Raymond"));
         assertEquals(user1, user2);
         assertEquals(user1.hashCode(), user2.hashCode());
 
@@ -78,7 +108,7 @@ public class UserTest
         assertFalse(user3.equals(user4));
         assertFalse(user3.hashCode() == user4.hashCode());
 
-        user1.getPrincipals().add(new X500Principal("cn=aaa,ou=ca"));
+        user1.getIdentities().add(new X500Principal("cn=aaa,ou=ca"));
         assertEquals(user1, user2);
         assertEquals(user1.hashCode(), user2.hashCode());
 
@@ -93,8 +123,7 @@ public class UserTest
         
         // visual test of toString
         System.out.println(user1);
-        System.out.println(new PersonalDetails("Joe", "Raymond",
-                "jr@email.com", "123 Street", "CADC", "Victoria", "CA"));
+        System.out.println(new PersonalDetails("Joe", "Raymond"));
         System.out.println(new PosixDetails(12, 23,"/home/myhome"));
         
     }
@@ -116,8 +145,7 @@ public class UserTest
         thrown = false;
         try
         {
-            new PersonalDetails(null, "Raymond",
-                    "jr@email.com", "123 Street", "CADC", "Victoria", "CA");
+            new PersonalDetails(null, "Raymond");
         }
         catch(IllegalArgumentException e)
         {
@@ -128,8 +156,7 @@ public class UserTest
         thrown = false;
         try
         {
-            new PersonalDetails("Joe", null,
-                    "jr@email.com", "123 Street", "CADC", "Victoria", "CA");
+            new PersonalDetails("Joe", null);
         }
         catch(IllegalArgumentException e)
         {
@@ -137,65 +164,6 @@ public class UserTest
         }
         assertTrue(thrown);
         
-        thrown = false;
-        try
-        {
-            new PersonalDetails("Joe", "Raymond",
-                    null, "123 Street", "CADC", "Victoria", "CA");
-        }
-        catch(IllegalArgumentException e)
-        {
-            thrown = true;
-        }
-        assertTrue(thrown);
-        
-        thrown = false;
-        try
-        {
-            new PersonalDetails("Joe", "Raymond",
-                    "jr@email.com", null, "CADC", "Victoria", "CA");
-        }
-        catch(IllegalArgumentException e)
-        {
-            thrown = true;
-        }
-        assertTrue(thrown);
-        
-        thrown = false;
-        try
-        {
-            new PersonalDetails("Joe", "Raymond",
-                    "jr@email.com", "123 Street", null, "Victoria", "CA");
-        }
-        catch(IllegalArgumentException e)
-        {
-            thrown = true;
-        }
-        assertTrue(thrown);
-        
-        thrown = false;
-        try
-        {
-            new PersonalDetails("Joe", "Raymond",
-                    "jr@email.com", "123 Street", "CADC", null, "CA");
-        }
-        catch(IllegalArgumentException e)
-        {
-            thrown = true;
-        }
-        assertTrue(thrown);
-        
-        thrown = false;
-        try
-        {
-            new PersonalDetails("Joe", "Raymond",
-                    "jr@email.com", "123 Street", "CADC", "Victoria", null);
-        }
-        catch(IllegalArgumentException e)
-        {
-            thrown = true;
-        }
-        assertTrue(thrown);
         
         thrown = false;
         try
@@ -230,4 +198,19 @@ public class UserTest
         }
         assertTrue(thrown);
     }
+
+    @Test
+    public void getDetails() throws Exception
+    {
+        final User<HttpPrincipal> testSubject =
+                new User<HttpPrincipal>(new HttpPrincipal("test"));
+
+        testSubject.details.add(new PersonalDetails("First", "Last"));
+
+        assertTrue("Should be empty.",
+                   testSubject.getDetails(PosixDetails.class).isEmpty());
+
+        assertEquals("Should be 1.", 1,
+                     testSubject.getDetails(PersonalDetails.class).size());
+    }
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..3025fb37678ac4fc3d0f464e6b320f9ced4ebcaf
--- /dev/null
+++ b/projects/cadcAccessControl/test/src/ca/nrc/cadc/ac/client/GMSClientTest.java
@@ -0,0 +1,245 @@
+/*
+ ************************************************************************
+ *******************  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 java.net.URI;
+import java.net.URL;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.security.auth.Subject;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.Logger;
+import org.junit.Assert;
+import org.junit.Test;
+
+import ca.nrc.cadc.ac.AC;
+import ca.nrc.cadc.ac.Group;
+import ca.nrc.cadc.ac.Role;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.reg.client.RegistryClient;
+import ca.nrc.cadc.util.Log4jInit;
+
+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 testUserIsSubject()
+    {
+        try
+        {
+            Subject subject = new Subject();
+            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");
+            GMSClient client = new GMSClient(baseURL.toString());
+
+            Assert.assertFalse(client.userIsSubject(null, null));
+            Assert.assertFalse(client.userIsSubject(userID, null));
+            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));
+        }
+        catch (Throwable t)
+        {
+            log.error("Unexpected exception", t);
+            Assert.fail("Unexpected exception: " + t.getMessage());
+        }
+    }
+    
+    @Test
+    public void testGroupCaching()
+    {
+        try
+        {
+            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");
+            final GMSClient client = new GMSClient(baseURL.toString());
+
+            Subject.doAs(subject, new PrivilegedExceptionAction<Object>()
+                {
+                    @Override
+                    public Object run() throws Exception
+                    {
+
+                        List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
+                        Assert.assertNull("Cache should be null", initial);
+
+                        List<Group> expected = new ArrayList<Group>();
+                        Group group1 = new Group("1");
+                        Group group2 = new Group("2");
+                        expected.add(group1);
+                        expected.add(group2);
+
+                        client.setCachedGroups(test1UserID, expected, Role.MEMBER);
+
+                        List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
+                        Assert.assertEquals("Wrong cached groups", expected, actual);
+                        
+                        // check against another role
+                        actual = client.getCachedGroups(test1UserID, Role.OWNER);
+                        Assert.assertNull("Cache should be null", actual);
+                        
+                        // check against another userid
+                        final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
+                        actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
+                        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>()
+                    {
+                        @Override
+                        public Object run() throws Exception
+                        {
+
+                            List<Group> initial = client.getCachedGroups(test2UserID, Role.MEMBER);
+                            Assert.assertNull("Cache should be null", initial);
+
+                            List<Group> expected = new ArrayList<Group>();
+                            Group group1 = new Group("1");
+                            Group group2 = new Group("2");
+                            expected.add(group1);
+                            expected.add(group2);
+
+                            client.setCachedGroups(test2UserID, expected, Role.MEMBER);
+
+                            List<Group> actual = client.getCachedGroups(test2UserID, Role.MEMBER);
+                            Assert.assertEquals("Wrong cached groups", expected, actual);
+                            
+                            // check against another role
+                            actual = client.getCachedGroups(test2UserID, Role.OWNER);
+                            Assert.assertNull("Cache should be null", actual);
+                            
+                            // check against another userid
+                            final HttpPrincipal anotherUserID = new HttpPrincipal("anotheruser");
+                            actual = client.getCachedGroups(anotherUserID, Role.MEMBER);
+                            Assert.assertNull("Cache should be null", actual);
+
+                            return null;
+                        }
+                    });
+
+            // do the same without a subject
+
+            List<Group> initial = client.getCachedGroups(test1UserID, Role.MEMBER);
+            Assert.assertNull("Cache should be null", initial);
+
+            List<Group> newgroups = new ArrayList<Group>();
+            Group group1 = new Group("1");
+            Group group2 = new Group("2");
+            newgroups.add(group1);
+            newgroups.add(group2);
+
+            client.setCachedGroups(test1UserID, newgroups, Role.MEMBER);
+
+            List<Group> actual = client.getCachedGroups(test1UserID, Role.MEMBER);
+            Assert.assertNull("Cache should still be null", actual);
+        }
+        catch (Throwable t)
+        {
+            log.error("Unexpected exception", t);
+            Assert.fail("Unexpected exception: " + t.getMessage());
+        }
+    }
+
+}