From 6a934289109f5bc0f52cbb63596f12b67bd3c198 Mon Sep 17 00:00:00 2001
From: Endi Sukma Dewata <edewata@redhat.com>
Date: Sat, 19 May 2012 11:12:12 -0500
Subject: [PATCH] Added group REST service.

The group REST service is based on UsrGrpAdminServlet. It provides an interface
to manage groups and group members.

Ticket #160
---
 base/common/src/CMakeLists.txt                     |    8 +
 .../src/com/netscape/certsrv/common/ScopeDef.java  |    1 +
 .../netscape/certsrv/group/GroupCollection.java    |   65 ++++
 .../src/com/netscape/certsrv/group/GroupData.java  |   99 +++++
 .../certsrv/group/GroupMemberCollection.java       |   83 +++++
 .../netscape/certsrv/group/GroupMemberData.java    |   83 +++++
 .../certsrv/group/GroupMemberResource.java         |   62 ++++
 .../com/netscape/certsrv/group/GroupResource.java  |   69 ++++
 .../com/netscape/certsrv/usrgrp/IUGSubsystem.java  |   11 +
 .../servlet/admin/GroupMemberResourceService.java  |  380 ++++++++++++++++++++
 .../cms/servlet/admin/GroupResourceService.java    |  335 +++++++++++++++++
 .../com/netscape/cmscore/usrgrp/UGSubsystem.java   |   27 ++
 12 files changed, 1223 insertions(+), 0 deletions(-)
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupCollection.java
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupData.java
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupMemberCollection.java
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupMemberData.java
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupMemberResource.java
 create mode 100644 base/common/src/com/netscape/certsrv/group/GroupResource.java
 create mode 100644 base/common/src/com/netscape/cms/servlet/admin/GroupMemberResourceService.java
 create mode 100644 base/common/src/com/netscape/cms/servlet/admin/GroupResourceService.java

diff --git a/base/common/src/CMakeLists.txt b/base/common/src/CMakeLists.txt
index 26a8397c9a9c53f30cbcc9261b4e43db4a3e227a..42a3a54ed43f9a3b5b5e6ef6312d1df0f7985fcf 100644
--- a/base/common/src/CMakeLists.txt
+++ b/base/common/src/CMakeLists.txt
@@ -141,6 +141,12 @@ set(pki-certsrv_java_SRCS
     com/netscape/certsrv/common/ConfigConstants.java
     com/netscape/certsrv/common/OpDef.java
     com/netscape/certsrv/common/Constants.java
+    com/netscape/certsrv/group/GroupCollection.java
+    com/netscape/certsrv/group/GroupData.java
+    com/netscape/certsrv/group/GroupMemberCollection.java
+    com/netscape/certsrv/group/GroupMemberData.java
+    com/netscape/certsrv/group/GroupMemberResource.java
+    com/netscape/certsrv/group/GroupResource.java
     com/netscape/certsrv/user/UserCertCollection.java
     com/netscape/certsrv/user/UserCertData.java
     com/netscape/certsrv/user/UserCertResource.java
@@ -622,6 +628,8 @@ set(pki-cms_java_SRCS
     com/netscape/cms/servlet/admin/AdminResources.java
     com/netscape/cms/servlet/admin/SystemCertificateResource.java
     com/netscape/cms/servlet/admin/SystemCertificateResourceService.java
+    com/netscape/cms/servlet/admin/GroupMemberResourceService.java
+    com/netscape/cms/servlet/admin/GroupResourceService.java
     com/netscape/cms/servlet/admin/UserCertResourceService.java
     com/netscape/cms/servlet/admin/UserResourceService.java
     com/netscape/cms/servlet/key/DisplayBySerial.java
diff --git a/base/common/src/com/netscape/certsrv/common/ScopeDef.java b/base/common/src/com/netscape/certsrv/common/ScopeDef.java
index e8919de5e0b8fa3a1f124e3225dcd41f7ee95051..017d082c8d855e664568029b7b72de15701fb384 100644
--- a/base/common/src/com/netscape/certsrv/common/ScopeDef.java
+++ b/base/common/src/com/netscape/certsrv/common/ScopeDef.java
@@ -28,6 +28,7 @@ public interface ScopeDef {
 
     // users and groups
     public final static String SC_GROUPS = "groups";
+    public final static String SC_GROUP_MEMBERS = "members";
     public final static String SC_USERS = "users";
     public final static String SC_USER_CERTS = "certs";
 
diff --git a/base/common/src/com/netscape/certsrv/group/GroupCollection.java b/base/common/src/com/netscape/certsrv/group/GroupCollection.java
new file mode 100644
index 0000000000000000000000000000000000000000..26d108e7b59479da74c79da228a45e5f8ede76bd
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupCollection.java
@@ -0,0 +1,65 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementRef;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+
+/**
+ * @author Endi S. Dewata
+ */
+@XmlRootElement(name="Groups")
+public class GroupCollection {
+
+    Collection<GroupData> groups = new ArrayList<GroupData>();
+    Collection<Link> links = new ArrayList<Link>();
+
+    @XmlElementRef
+    public Collection<GroupData> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(Collection<GroupData> groups) {
+        this.groups = groups;
+    }
+
+    public void addGroup(GroupData groupData) {
+        groups.add(groupData);
+    }
+
+    @XmlElement(name="Link")
+    public Collection<Link> getLinks() {
+        return links;
+    }
+
+    public void setLink(Collection<Link> links) {
+        this.links = links;
+    }
+
+    public void addLink(Link link) {
+        links.add(link);
+    }
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupData.java b/base/common/src/com/netscape/certsrv/group/GroupData.java
new file mode 100644
index 0000000000000000000000000000000000000000..a21ad7f01ca34e4ef05f58947fd670a6a95d3836
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupData.java
@@ -0,0 +1,99 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import javax.ws.rs.FormParam;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+import com.netscape.certsrv.common.Constants;
+
+/**
+ * @author Endi S. Dewata
+ */
+@XmlRootElement(name="Group")
+public class GroupData {
+
+    String id;
+    String description;
+
+    Link link;
+
+    @XmlAttribute(name="id")
+    public String getID() {
+        return id;
+    }
+
+    public void setID(String id) {
+        this.id = id;
+    }
+
+    @FormParam(Constants.PR_GROUP_DESC)
+    @XmlElement(name="Description")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @XmlElement(name="Link")
+    public Link getLink() {
+        return link;
+    }
+
+    public void setLink(Link link) {
+        this.link = link;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((description == null) ? 0 : description.hashCode());
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        GroupData other = (GroupData) obj;
+        if (description == null) {
+            if (other.description != null)
+                return false;
+        } else if (!description.equals(other.description))
+            return false;
+        if (id == null) {
+            if (other.id != null)
+                return false;
+        } else if (!id.equals(other.id))
+            return false;
+        return true;
+    }
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupMemberCollection.java b/base/common/src/com/netscape/certsrv/group/GroupMemberCollection.java
new file mode 100644
index 0000000000000000000000000000000000000000..d19d939ade93789ee51aabd74349758acfe1aded
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupMemberCollection.java
@@ -0,0 +1,83 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+/**
+ * @author Endi S. Dewata
+ */
+@XmlRootElement(name="GroupMembers")
+public class GroupMemberCollection {
+
+    Collection<GroupMemberData> members = new ArrayList<GroupMemberData>();
+    Collection<Link> links = new ArrayList<Link>();
+
+    @XmlElement(name="Member")
+    public Collection<GroupMemberData> getMembers() {
+        return members;
+    }
+
+    public void setMembers(Collection<GroupMemberData> members) {
+        this.members = members;
+    }
+
+    public void addMember(GroupMemberData member) {
+        members.add(member);
+    }
+
+    @XmlElement(name="Link")
+    public Collection<Link> getLinks() {
+        return links;
+    }
+
+    public void setLink(Collection<Link> links) {
+        this.links = links;
+    }
+
+    public void addLink(Link link) {
+        links.add(link);
+    }
+
+    public static void main(String args[]) throws Exception {
+
+        GroupMemberCollection response = new GroupMemberCollection();
+
+        GroupMemberData member1 = new GroupMemberData();
+        member1.setID("User 1");
+        response.addMember(member1);
+
+        GroupMemberData member2 = new GroupMemberData();
+        member2.setID("User 2");
+        response.addMember(member2);
+
+        JAXBContext context = JAXBContext.newInstance(GroupMemberCollection.class);
+        Marshaller marshaller = context.createMarshaller();
+        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+        marshaller.marshal(response, System.out);
+    }
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupMemberData.java b/base/common/src/com/netscape/certsrv/group/GroupMemberData.java
new file mode 100644
index 0000000000000000000000000000000000000000..11f3a2147f2e93831857668ec4316fd588505d42
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupMemberData.java
@@ -0,0 +1,83 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import javax.ws.rs.FormParam;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+import com.netscape.certsrv.common.Constants;
+
+/**
+ * @author Endi S. Dewata
+ */
+@XmlRootElement(name="GroupMember")
+public class GroupMemberData {
+
+    String id;
+
+    Link link;
+
+    @FormParam(Constants.PR_GROUP_USER)
+    @XmlAttribute(name="id")
+    public String getID() {
+        return id;
+    }
+
+    public void setID(String id) {
+        this.id = id;
+    }
+
+    @XmlElement(name="Link")
+    public Link getLink() {
+        return link;
+    }
+
+    public void setLink(Link link) {
+        this.link = link;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        GroupMemberData other = (GroupMemberData) obj;
+        if (id == null) {
+            if (other.id != null)
+                return false;
+        } else if (!id.equals(other.id))
+            return false;
+        return true;
+    }
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupMemberResource.java b/base/common/src/com/netscape/certsrv/group/GroupMemberResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..51370f5731f4597ee0e42a318a116e37c1016a86
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupMemberResource.java
@@ -0,0 +1,62 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.ClientResponseType;
+
+/**
+ * @author Endi S. Dewata
+ */
+@Path("/groups/{groupID}/members")
+public interface GroupMemberResource {
+
+    @GET
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public GroupMemberCollection findGroupMembers(
+            @PathParam("groupID") String groupID,
+            @QueryParam("start") Integer start,
+            @QueryParam("size") Integer size);
+
+    @POST
+    @ClientResponseType(entityType=GroupMemberData.class)
+    @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public Response addGroupMember(@PathParam("groupID") String groupID, String memberID);
+
+    @GET
+    @Path("/{memberID}")
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public GroupMemberData getGroupMember(@PathParam("groupID") String groupID, @PathParam("memberID") String memberID);
+
+    @DELETE
+    @Path("/{memberID}")
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public void removeGroupMember(@PathParam("groupID") String groupID, @PathParam("memberID") String memberID);
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupResource.java b/base/common/src/com/netscape/certsrv/group/GroupResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..5889048ea6546c57b3b57c7afeaa865b1ee9f83b
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/group/GroupResource.java
@@ -0,0 +1,69 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.group;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.ClientResponseType;
+
+/**
+ * @author Endi S. Dewata
+ */
+@Path("/groups")
+public interface GroupResource {
+
+    @GET
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public GroupCollection findGroups(
+            @QueryParam("filter") String filter,
+            @QueryParam("start") Integer start,
+            @QueryParam("size") Integer size);
+
+    @POST
+    @ClientResponseType(entityType=GroupData.class)
+    @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public Response addGroup(GroupData groupData);
+
+    @GET
+    @Path("/{groupID}")
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public GroupData getGroup(@PathParam("groupID") String groupID);
+
+    @POST
+    @Path("/{groupID}")
+    @ClientResponseType(entityType=GroupData.class)
+    @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public Response modifyGroup(@PathParam("groupID") String groupID, GroupData groupData);
+
+    @DELETE
+    @Path("/{groupID}")
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public void removeGroup(@PathParam("groupID") String groupID);
+}
diff --git a/base/common/src/com/netscape/certsrv/usrgrp/IUGSubsystem.java b/base/common/src/com/netscape/certsrv/usrgrp/IUGSubsystem.java
index 60a1f70ead9ba7f7fc56f220c310c60163935fd3..c101187c497f8aa8e7d8b7aae67ec91ca70f9030 100644
--- a/base/common/src/com/netscape/certsrv/usrgrp/IUGSubsystem.java
+++ b/base/common/src/com/netscape/certsrv/usrgrp/IUGSubsystem.java
@@ -205,6 +205,17 @@ public interface IUGSubsystem extends ISubsystem, IUsrGrp {
     public void modifyGroup(IGroup group) throws EUsrGrpException;
 
     /**
+     * Adds the user with the given id into the given group
+     *
+     * @param grp the given group
+     * @param userid the given user id
+     * @exception EUsrGrpException thrown when failed to add the user into
+     *                the given group
+     */
+    public void addUserToGroup(IGroup grp, String userid)
+            throws EUsrGrpException;
+
+    /**
      * Removes the user with the given id from the given group
      *
      * @param grp the given group
diff --git a/base/common/src/com/netscape/cms/servlet/admin/GroupMemberResourceService.java b/base/common/src/com/netscape/cms/servlet/admin/GroupMemberResourceService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c192815aa939d7446b5f7d8cc685de735b8c0fbf
--- /dev/null
+++ b/base/common/src/com/netscape/cms/servlet/admin/GroupMemberResourceService.java
@@ -0,0 +1,380 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.cms.servlet.admin;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.Enumeration;
+import java.util.Map;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+import com.netscape.certsrv.apps.CMS;
+import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.base.IConfigStore;
+import com.netscape.certsrv.base.SessionContext;
+import com.netscape.certsrv.common.OpDef;
+import com.netscape.certsrv.common.ScopeDef;
+import com.netscape.certsrv.group.GroupMemberCollection;
+import com.netscape.certsrv.group.GroupMemberData;
+import com.netscape.certsrv.group.GroupMemberResource;
+import com.netscape.certsrv.logging.AuditFormat;
+import com.netscape.certsrv.logging.IAuditor;
+import com.netscape.certsrv.logging.ILogger;
+import com.netscape.certsrv.user.UserResource;
+import com.netscape.certsrv.usrgrp.IGroup;
+import com.netscape.certsrv.usrgrp.IUGSubsystem;
+import com.netscape.cms.servlet.base.CMSException;
+import com.netscape.cms.servlet.base.CMSResourceService;
+
+/**
+ * @author Endi S. Dewata
+ */
+public class GroupMemberResourceService extends CMSResourceService implements GroupMemberResource {
+
+    public final static int DEFAULT_SIZE = 20;
+
+    public final static String MULTI_ROLE_ENABLE = "multiroles.enable";
+    public final static String MULTI_ROLE_ENFORCE_GROUP_LIST = "multiroles.false.groupEnforceList";
+
+    public static String[] multiRoleGroupEnforceList;
+
+    public IUGSubsystem userGroupManager = (IUGSubsystem) CMS.getSubsystem(CMS.SUBSYSTEM_UG);
+
+    public GroupMemberData createGroupMemberData(String memberID) throws Exception {
+
+        GroupMemberData groupMemberData = new GroupMemberData();
+
+        groupMemberData.setID(memberID);
+
+        String userID = URLEncoder.encode(memberID, "UTF-8");
+        URI uri = uriInfo.getBaseUriBuilder().path(UserResource.class).path("{userID}").build(userID);
+        groupMemberData.setLink(new Link("self", uri));
+
+        return groupMemberData;
+    }
+
+    @Override
+    public GroupMemberCollection findGroupMembers(String groupID, Integer start, Integer size) {
+        try {
+            start = start == null ? 0 : start;
+            size = size == null ? DEFAULT_SIZE : size;
+
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+            if (group == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("USRGRP_SRVLT_GROUP_NOT_EXIST"));
+                throw new CMSException(getUserMessage("CMS_USRGRP_SRVLT_GROUP_NOT_EXIST"));
+            }
+
+            GroupMemberCollection response = new GroupMemberCollection();
+
+            Enumeration<String> members = group.getMemberNames();
+
+            int i = 0;
+
+            // skip to the start of the page
+            for ( ; i<start && members.hasMoreElements(); i++) members.nextElement();
+
+            // return entries up to the page size
+            for ( ; i<start+size && members.hasMoreElements(); i++) {
+                String memberID = members.nextElement();
+                response.addMember(createGroupMemberData(memberID));
+            }
+
+            // count the total entries
+            for ( ; members.hasMoreElements(); i++) members.nextElement();
+
+            if (start > 0) {
+                URI uri = uriInfo.getRequestUriBuilder().replaceQueryParam("start", Math.max(start-size, 0)).build();
+                response.addLink(new Link("prev", uri));
+            }
+
+            if (start+size < i) {
+                URI uri = uriInfo.getRequestUriBuilder().replaceQueryParam("start", start+size).build();
+                response.addLink(new Link("next", uri));
+            }
+
+            return response;
+
+        } catch (CMSException e) {
+            throw e;
+
+        } catch (Exception e) {
+            CMS.debug(e);
+            throw new CMSException(getUserMessage("CMS_INTERNAL_ERROR"));
+        }
+    }
+
+    @Override
+    public Response addGroupMember(String groupID, String memberID) {
+        GroupMemberData groupMemberData = new GroupMemberData();
+        groupMemberData.setID(memberID);
+        return addGroupMember(groupID, groupMemberData);
+    }
+
+    public Response addGroupMember(String groupID, GroupMemberData groupMemberData) {
+
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+            if (group == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("USRGRP_SRVLT_GROUP_NOT_EXIST"));
+                throw new CMSException(getUserMessage("CMS_USRGRP_SRVLT_GROUP_NOT_EXIST"));
+            }
+
+            String memberID = groupMemberData.getID();
+            boolean multiRole = true;
+
+            try {
+                IConfigStore config = CMS.getConfigStore();
+                multiRole = config.getBoolean(MULTI_ROLE_ENABLE);
+            } catch (Exception e) {
+                // ignore
+            }
+
+            if (multiRole) {
+                // a user can be a member of multiple groups
+                userGroupManager.addUserToGroup(group, memberID);
+
+            } else {
+                // a user can be a member of at most one group in the enforce list
+                if (isGroupInMultiRoleEnforceList(groupID)) {
+                    // make sure the user is not already a member in another group in the list
+                    if (!isDuplicate(groupID, memberID)) {
+                        userGroupManager.addUserToGroup(group, memberID);
+                    } else {
+                        throw new EBaseException(CMS.getUserMessage("CMS_BASE_DUPLICATE_ROLES", memberID));
+                    }
+
+                } else {
+                    // the user can be a member of multiple groups outside the list
+                    userGroupManager.addUserToGroup(group, memberID);
+                }
+            }
+
+            // for audit log
+            SessionContext sContext = SessionContext.getContext();
+            String adminId = (String) sContext.get(SessionContext.USER_ID);
+
+            logger.log(ILogger.EV_AUDIT, ILogger.S_USRGRP,
+                    AuditFormat.LEVEL, AuditFormat.ADDUSERGROUPFORMAT,
+                    new Object[] { adminId, memberID, groupID });
+
+            auditAddGroupMember(groupID, groupMemberData, ILogger.SUCCESS);
+
+            // read the data back
+            groupMemberData = getGroupMember(groupID, memberID);
+
+            return Response
+                    .created(groupMemberData.getLink().getHref())
+                    .entity(groupMemberData)
+                    .type(MediaType.APPLICATION_XML)
+                    .build();
+
+        } catch (CMSException e) {
+            auditAddGroupMember(groupID, groupMemberData, ILogger.FAILURE);
+            throw e;
+
+        } catch (Exception e) {
+            log(ILogger.LL_FAILURE, e.toString());
+            auditAddGroupMember(groupID, groupMemberData, ILogger.FAILURE);
+            throw new CMSException(getUserMessage("CMS_USRGRP_USER_ADD_FAILED"));
+        }
+    }
+
+    public boolean isGroupInMultiRoleEnforceList(String groupID) {
+
+        if (groupID == null || groupID.equals("")) {
+            return true;
+        }
+
+        String groupList = null;
+        if (multiRoleGroupEnforceList == null) {
+            try {
+                IConfigStore config = CMS.getConfigStore();
+                groupList = config.getString(MULTI_ROLE_ENFORCE_GROUP_LIST);
+            } catch (Exception e) {
+                // ignore
+            }
+
+            if (groupList != null && !groupList.equals("")) {
+                multiRoleGroupEnforceList = groupList.split(",");
+                for (int j = 0; j < multiRoleGroupEnforceList.length; j++) {
+                    multiRoleGroupEnforceList[j] = multiRoleGroupEnforceList[j].trim();
+                }
+            }
+        }
+
+        if (multiRoleGroupEnforceList == null)
+            return true;
+
+        for (int i = 0; i < multiRoleGroupEnforceList.length; i++) {
+            if (groupID.equals(multiRoleGroupEnforceList[i])) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public boolean isDuplicate(String groupID, String memberID) {
+
+        // Let's not mess with users that are already a member of this group
+        try {
+            boolean isMember = userGroupManager.isMemberOf(memberID, groupID);
+            if (isMember == true) return false;
+
+        } catch (Exception e) {
+            // ignore
+        }
+
+        try {
+            Enumeration<IGroup> groups = userGroupManager.listGroups("*");
+            while (groups.hasMoreElements()) {
+                IGroup group = groups.nextElement();
+                String name = group.getName();
+
+                Enumeration<IGroup> g = userGroupManager.findGroups(name);
+                IGroup g1 = g.nextElement();
+
+                if (!name.equals(groupID)) {
+                    if (isGroupInMultiRoleEnforceList(name)) {
+                        Enumeration<String> members = g1.getMemberNames();
+                        while (members.hasMoreElements()) {
+                            String m1 = members.nextElement();
+                            if (m1.equals(memberID))
+                                return true;
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+
+        return false;
+    }
+
+    @Override
+    public GroupMemberData getGroupMember(String groupID, String memberID) {
+
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+            if (group == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("USRGRP_SRVLT_GROUP_NOT_EXIST"));
+                throw new CMSException(getUserMessage("CMS_USRGRP_SRVLT_GROUP_NOT_EXIST"));
+            }
+
+            Enumeration<String> e = group.getMemberNames();
+            while (e.hasMoreElements()) {
+                String memberName = e.nextElement();
+                if (!memberName.equals(memberID)) continue;
+
+                GroupMemberData groupMemberData = createGroupMemberData(memberID);
+                return groupMemberData;
+            }
+
+            throw new CMSException("Group member not found");
+
+        } catch (CMSException e) {
+            throw e;
+
+        } catch (Exception e) {
+            log(ILogger.LL_FAILURE, e.toString());
+            throw new CMSException(e.getMessage());
+        }
+    }
+
+    @Override
+    public void removeGroupMember(String groupID, String memberID) {
+        GroupMemberData groupMemberData = new GroupMemberData();
+        groupMemberData.setID(memberID);
+        removeGroupMember(groupID, groupMemberData);
+    }
+
+    public void removeGroupMember(String groupID, GroupMemberData groupMemberData) {
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+            if (group == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("USRGRP_SRVLT_GROUP_NOT_EXIST"));
+                throw new CMSException(getUserMessage("CMS_USRGRP_SRVLT_GROUP_NOT_EXIST"));
+            }
+
+            String member = groupMemberData.getID();
+            userGroupManager.removeUserFromGroup(group, member);
+
+            // for audit log
+            SessionContext sContext = SessionContext.getContext();
+            String adminId = (String) sContext.get(SessionContext.USER_ID);
+
+            logger.log(ILogger.EV_AUDIT, ILogger.S_USRGRP,
+                    AuditFormat.LEVEL, AuditFormat.REMOVEUSERGROUPFORMAT,
+                    new Object[] { adminId, member, groupID });
+
+            auditDeleteGroupMember(groupID, groupMemberData, ILogger.SUCCESS);
+
+        } catch (CMSException e) {
+            auditDeleteGroupMember(groupID, groupMemberData, ILogger.FAILURE);
+            throw e;
+
+        } catch (Exception e) {
+            log(ILogger.LL_FAILURE, e.toString());
+            auditDeleteGroupMember(groupID, groupMemberData, ILogger.FAILURE);
+            throw new CMSException(getUserMessage("CMS_USRGRP_USER_ADD_FAILED"));
+        }
+    }
+
+    public void log(int level, String message) {
+        log(ILogger.S_USRGRP, level, message);
+    }
+
+    public void auditAddGroupMember(String groupID, GroupMemberData groupMemberData, String status) {
+        audit(OpDef.OP_ADD, groupID, getParams(groupMemberData), status);
+    }
+
+    public void auditDeleteGroupMember(String groupID, GroupMemberData groupMemberData, String status) {
+        audit(OpDef.OP_DELETE, groupID, getParams(groupMemberData), status);
+    }
+
+    public void audit(String type, String id, Map<String, String> params, String status) {
+        audit(IAuditor.LOGGING_SIGNED_AUDIT_CONFIG_ROLE, ScopeDef.SC_GROUP_MEMBERS, type, id, params, status);
+    }
+}
diff --git a/base/common/src/com/netscape/cms/servlet/admin/GroupResourceService.java b/base/common/src/com/netscape/cms/servlet/admin/GroupResourceService.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce665ae3e0a3d6deebfc466f59a7d313d0b5d910
--- /dev/null
+++ b/base/common/src/com/netscape/cms/servlet/admin/GroupResourceService.java
@@ -0,0 +1,335 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2012 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.cms.servlet.admin;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.Enumeration;
+import java.util.Map;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.commons.lang.StringUtils;
+import org.jboss.resteasy.plugins.providers.atom.Link;
+
+import com.netscape.certsrv.apps.CMS;
+import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.common.OpDef;
+import com.netscape.certsrv.common.ScopeDef;
+import com.netscape.certsrv.group.GroupCollection;
+import com.netscape.certsrv.group.GroupData;
+import com.netscape.certsrv.group.GroupResource;
+import com.netscape.certsrv.logging.IAuditor;
+import com.netscape.certsrv.logging.ILogger;
+import com.netscape.certsrv.usrgrp.IGroup;
+import com.netscape.certsrv.usrgrp.IUGSubsystem;
+import com.netscape.cms.servlet.base.CMSException;
+import com.netscape.cms.servlet.base.CMSResourceService;
+import com.netscape.cmsutil.ldap.LDAPUtil;
+
+/**
+ * @author Endi S. Dewata
+ */
+public class GroupResourceService extends CMSResourceService implements GroupResource {
+
+    public final static int DEFAULT_SIZE = 20;
+
+    public IUGSubsystem userGroupManager = (IUGSubsystem) CMS.getSubsystem(CMS.SUBSYSTEM_UG);
+
+    public GroupData createGroupData(IGroup group) throws Exception {
+
+        GroupData groupData = new GroupData();
+
+        String id = group.getGroupID();
+        if (!StringUtils.isEmpty(id)) groupData.setID(id);
+
+        String description = group.getDescription();
+        if (!StringUtils.isEmpty(description)) groupData.setDescription(description);
+
+        String groupID = URLEncoder.encode(groupData.getID(), "UTF-8");
+        URI uri = uriInfo.getBaseUriBuilder().path(GroupResource.class).path("{groupID}").build(groupID);
+        groupData.setLink(new Link("self", uri));
+
+        return groupData;
+    }
+
+    /**
+     * Searches for users in LDAP directory.
+     *
+     * Request/Response Syntax:
+     * http://warp.mcom.com/server/certificate/columbo/design/
+     * ui/admin-protocol-definition.html#user-admin
+     */
+    @Override
+    public GroupCollection findGroups(String filter, Integer start, Integer size) {
+        try {
+            filter = StringUtils.isEmpty(filter) ? "*" : "*"+LDAPUtil.escapeFilter(filter)+"*";
+            start = start == null ? 0 : start;
+            size = size == null ? DEFAULT_SIZE : size;
+
+            Enumeration<IGroup> groups = userGroupManager.listGroups(filter);
+
+            GroupCollection response = new GroupCollection();
+
+            int i = 0;
+
+            // skip to the start of the page
+            for ( ; i<start && groups.hasMoreElements(); i++) groups.nextElement();
+
+            // return entries up to the page size
+            for ( ; i<start+size && groups.hasMoreElements(); i++) {
+                IGroup group = groups.nextElement();
+                response.addGroup(createGroupData(group));
+            }
+
+            // count the total entries
+            for ( ; groups.hasMoreElements(); i++) groups.nextElement();
+
+            if (start > 0) {
+                URI uri = uriInfo.getRequestUriBuilder().replaceQueryParam("start", Math.max(start-size, 0)).build();
+                response.addLink(new Link("prev", uri));
+            }
+
+            if (start+size < i) {
+                URI uri = uriInfo.getRequestUriBuilder().replaceQueryParam("start", start+size).build();
+                response.addLink(new Link("next", uri));
+            }
+
+            return response;
+
+        } catch (Exception e) {
+            throw new CMSException(getUserMessage("CMS_INTERNAL_ERROR"));
+        }
+    }
+
+    /**
+     * finds a group
+     * Request/Response Syntax:
+     * http://warp.mcom.com/server/certificate/columbo/design/
+     * ui/admin-protocol-definition.html#user-admin
+     */
+    @Override
+    public GroupData getGroup(String groupID) {
+
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+            if (group == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("USRGRP_SRVLT_GROUP_NOT_EXIST"));
+                throw new CMSException(getUserMessage("CMS_USRGRP_SRVLT_GROUP_NOT_EXIST"));
+            }
+
+            return createGroupData(group);
+
+        } catch (CMSException e) {
+            throw e;
+
+        } catch (Exception e) {
+            throw new CMSException(getUserMessage("CMS_INTERNAL_ERROR"));
+        }
+    }
+
+    /**
+     * Adds a new group in local scope.
+     * <P>
+     *
+     * Request/Response Syntax: http://warp.mcom.com/server/certificate/columbo/design/
+     * ui/admin-protocol-definition.html#group
+     * <P>
+     *
+     * <ul>
+     * <li>signed.audit LOGGING_SIGNED_AUDIT_CONFIG_ROLE used when configuring role information (anything under
+     * users/groups)
+     * </ul>
+     */
+    @Override
+    public Response addGroup(GroupData groupData) {
+
+        String groupID = groupData.getID();
+
+        // ensure that any low-level exceptions are reported
+        // to the signed audit log and stored as failures
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.createGroup(groupID);
+
+            String description = groupData.getDescription();
+            if (description != null) {
+                group.set("description", description);
+            } else {
+                group.set("description", "");
+            }
+
+            // allow adding a group with no members
+            try {
+                userGroupManager.addGroup(group);
+
+                auditAddGroup(groupID, groupData, ILogger.SUCCESS);
+
+                // read the data back
+                groupData = getGroup(groupID);
+
+                return Response
+                        .created(groupData.getLink().getHref())
+                        .entity(groupData)
+                        .type(MediaType.APPLICATION_XML)
+                        .build();
+
+            } catch (Exception e) {
+                throw new CMSException(getUserMessage("CMS_USRGRP_GROUP_ADD_FAILED"));
+            }
+
+        } catch (CMSException e) {
+            auditAddGroup(groupID, groupData, ILogger.FAILURE);
+            throw e;
+
+        } catch (EBaseException e) {
+            auditAddGroup(groupID, groupData, ILogger.FAILURE);
+            throw new CMSException(e.getMessage());
+        }
+    }
+
+    /**
+     * modifies a group
+     * <P>
+     *
+     * last person of the super power group "Certificate Server Administrators" can never be removed.
+     * <P>
+     *
+     * http://warp.mcom.com/server/certificate/columbo/design/ ui/admin-protocol-definition.html#group
+     * <P>
+     *
+     * <ul>
+     * <li>signed.audit LOGGING_SIGNED_AUDIT_CONFIG_ROLE used when configuring role information (anything under
+     * users/groups)
+     * </ul>
+     */
+    @Override
+    public Response modifyGroup(String groupID, GroupData groupData) {
+
+        // ensure that any low-level exceptions are reported
+        // to the signed audit log and stored as failures
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            IGroup group = userGroupManager.getGroupFromName(groupID);
+
+            group.set("description", groupData.getDescription());
+
+            // allow adding a group with no members, except "Certificate
+            // Server Administrators"
+            try {
+                userGroupManager.modifyGroup(group);
+
+                auditModifyGroup(groupID, groupData, ILogger.SUCCESS);
+
+                // read the data back
+                groupData = getGroup(groupID);
+
+                return Response
+                        .ok(groupData)
+                        .type(MediaType.APPLICATION_XML)
+                        .build();
+
+            } catch (Exception e) {
+                log(ILogger.LL_FAILURE, e.toString());
+                throw new CMSException(getUserMessage("CMS_USRGRP_GROUP_MODIFY_FAILED"));
+            }
+
+        } catch (CMSException e) {
+            auditModifyGroup(groupID, groupData, ILogger.FAILURE);
+            throw e;
+
+        } catch (EBaseException e) {
+            auditModifyGroup(groupID, groupData, ILogger.FAILURE);
+            throw new CMSException(e.getMessage());
+        }
+    }
+
+    /**
+     * removes a group
+     * <P>
+     *
+     * Request/Response Syntax: http://warp.mcom.com/server/certificate/columbo/design/
+     * ui/admin-protocol-definition.html#group
+     * <P>
+     *
+     * <ul>
+     * <li>signed.audit LOGGING_SIGNED_AUDIT_CONFIG_ROLE used when configuring role information (anything under
+     * users/groups)
+     * </ul>
+     */
+    @Override
+    public void removeGroup(String groupID) {
+
+        // ensure that any low-level exceptions are reported
+        // to the signed audit log and stored as failures
+        try {
+            if (groupID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new CMSException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID"));
+            }
+
+            // if fails, let the exception fall through
+            userGroupManager.removeGroup(groupID);
+
+            auditDeleteGroup(groupID, ILogger.SUCCESS);
+
+        } catch (CMSException e) {
+            auditDeleteGroup(groupID, ILogger.FAILURE);
+            throw e;
+
+        } catch (EBaseException e) {
+            auditDeleteGroup(groupID, ILogger.FAILURE);
+            throw new CMSException(e.getMessage());
+        }
+    }
+
+    public void log(int level, String message) {
+        log(ILogger.S_USRGRP, level, message);
+    }
+
+    public void auditAddGroup(String groupID, GroupData groupData, String status) {
+        audit(OpDef.OP_ADD, groupID, getParams(groupData), status);
+    }
+
+    public void auditModifyGroup(String groupID, GroupData groupData, String status) {
+        audit(OpDef.OP_MODIFY, groupID, getParams(groupData), status);
+    }
+
+    public void auditDeleteGroup(String groupID, String status) {
+        audit(OpDef.OP_DELETE, groupID, null, status);
+    }
+
+    public void audit(String type, String id, Map<String, String> params, String status) {
+        audit(IAuditor.LOGGING_SIGNED_AUDIT_CONFIG_ROLE, ScopeDef.SC_GROUPS, type, id, params, status);
+    }
+}
diff --git a/base/common/src/com/netscape/cmscore/usrgrp/UGSubsystem.java b/base/common/src/com/netscape/cmscore/usrgrp/UGSubsystem.java
index 7cca7000252a65038e6f72ac6fa8fba6095f41a8..192f2f4835cd0a23a6d79c84997fd45927997db3 100644
--- a/base/common/src/com/netscape/cmscore/usrgrp/UGSubsystem.java
+++ b/base/common/src/com/netscape/cmscore/usrgrp/UGSubsystem.java
@@ -917,6 +917,33 @@ public final class UGSubsystem implements IUGSubsystem {
         return;
     }
 
+    public void addUserToGroup(IGroup grp, String userid)
+            throws EUsrGrpException {
+
+        LDAPConnection ldapconn = null;
+
+        try {
+            ldapconn = getConn();
+            String groupDN = "cn=" + grp.getGroupID() +
+                    "," + getGroupBaseDN();
+            LDAPAttribute memberAttr = new LDAPAttribute(
+                    "uniquemember", "uid=" + userid + "," + getUserBaseDN());
+            LDAPModification singleChange = new LDAPModification(
+                    LDAPModification.ADD, memberAttr);
+
+            ldapconn.modify(groupDN, singleChange);
+        } catch (LDAPException e) {
+            log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_USRGRP_ADD_USER_TO_GROUP", e.toString()));
+
+            throw new EUsrGrpException(CMS.getUserMessage("CMS_USRGRP_ADD_USER_FAIL"));
+        } catch (ELdapException e) {
+            log(ILogger.LL_FAILURE, CMS.getLogMessage("CMSCORE_USRGRP_ADD_USER_TO_GROUP", e.toString()));
+        } finally {
+            if (ldapconn != null)
+                returnConn(ldapconn);
+        }
+    }
+
     public void removeUserFromGroup(IGroup grp, String userid)
             throws EUsrGrpException {
 
-- 
1.7.7.6

