From fce902aaa7292a621144e61be33e1ccf88bac879 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Tue, 10 Dec 2013 20:10:06 -0500
Subject: [PATCH] Using PATCH method for modify operations.

Some modify operations have been modified to use HTTP PATCH method
since the request only contains changes to the resource, not the
entire resource. To replace the entire resource, separate replace
operations using HTTP PUT method will be used instead. The Backbone
library is using the same convention by default.

Ticket #654
---
 .../src/com/netscape/certsrv/base/PATCH.java       |  35 +++++
 .../com/netscape/certsrv/group/GroupResource.java  |   3 +-
 .../netscape/certsrv/tps/token/TokenClient.java    |   2 +-
 .../certsrv/tps/token/TokenModifyRequest.java      | 152 ---------------------
 .../netscape/certsrv/tps/token/TokenResource.java  |   7 +-
 .../com/netscape/certsrv/user/UserResource.java    |  11 +-
 .../netscape/cms/servlet/admin/GroupService.java   |   5 +-
 .../netscape/cms/servlet/admin/UserService.java    |  79 +++++++++++
 .../dogtagpki/server/tps/token/TokenService.java   |  21 +--
 9 files changed, 148 insertions(+), 167 deletions(-)
 create mode 100644 base/common/src/com/netscape/certsrv/base/PATCH.java
 delete mode 100644 base/common/src/com/netscape/certsrv/tps/token/TokenModifyRequest.java

diff --git a/base/common/src/com/netscape/certsrv/base/PATCH.java b/base/common/src/com/netscape/certsrv/base/PATCH.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f2af17f14987d00a8f526dcf2d5a6ad92c25051
--- /dev/null
+++ b/base/common/src/com/netscape/certsrv/base/PATCH.java
@@ -0,0 +1,35 @@
+// --- 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) 2013 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.certsrv.base;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.ws.rs.HttpMethod;
+
+/**
+ * http://brianoneill.blogspot.in/2011/11/patch-methods-on-jax-rs.html
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@HttpMethod("PATCH")
+public @interface PATCH {
+}
diff --git a/base/common/src/com/netscape/certsrv/group/GroupResource.java b/base/common/src/com/netscape/certsrv/group/GroupResource.java
index 12c90228b09da7222a0f7a2c73fc68716e11c979..aa26bebb1eaf5e550527917d21d4caf11f3fbc8b 100644
--- a/base/common/src/com/netscape/certsrv/group/GroupResource.java
+++ b/base/common/src/com/netscape/certsrv/group/GroupResource.java
@@ -33,6 +33,7 @@ import org.jboss.resteasy.annotations.ClientResponseType;
 
 import com.netscape.certsrv.acls.ACLMapping;
 import com.netscape.certsrv.authentication.AuthMethodMapping;
+import com.netscape.certsrv.base.PATCH;
 
 /**
  * @author Endi S. Dewata
@@ -60,7 +61,7 @@ public interface GroupResource {
     @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
     public GroupData getGroup(@PathParam("groupID") String groupID);
 
-    @POST
+    @PATCH
     @Path("{groupID}")
     @ClientResponseType(entityType=GroupData.class)
     @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenClient.java b/base/common/src/com/netscape/certsrv/tps/token/TokenClient.java
index 602e8f9660b81c6742d3fcf5f5dcfd9d00858174..e349acd1e5af660870919c3f24fde43359a9d412 100644
--- a/base/common/src/com/netscape/certsrv/tps/token/TokenClient.java
+++ b/base/common/src/com/netscape/certsrv/tps/token/TokenClient.java
@@ -56,7 +56,7 @@ public class TokenClient extends Client {
 
     public TokenData updateToken(String tokenID, TokenData tokenData) {
         @SuppressWarnings("unchecked")
-        ClientResponse<TokenData> response = (ClientResponse<TokenData>)resource.updateToken(tokenID, tokenData);
+        ClientResponse<TokenData> response = (ClientResponse<TokenData>)resource.replaceToken(tokenID, tokenData);
         return client.getEntity(response);
     }
 
diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenModifyRequest.java b/base/common/src/com/netscape/certsrv/tps/token/TokenModifyRequest.java
deleted file mode 100644
index b4168261f0855dace111c8222a3da4746b0d687b..0000000000000000000000000000000000000000
--- a/base/common/src/com/netscape/certsrv/tps/token/TokenModifyRequest.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// --- 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) 2013 Red Hat, Inc.
-// All rights reserved.
-// --- END COPYRIGHT BLOCK ---
-
-package com.netscape.certsrv.tps.token;
-
-import java.io.StringReader;
-import java.io.StringWriter;
-
-import javax.xml.bind.JAXBContext;
-import javax.xml.bind.Marshaller;
-import javax.xml.bind.Unmarshaller;
-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;
-
-/**
- * @author Endi S. Dewata
- */
-@XmlRootElement(name="TokenModifyRequest")
-public class TokenModifyRequest {
-
-    public static Marshaller marshaller;
-    public static Unmarshaller unmarshaller;
-
-    static {
-        try {
-            marshaller = JAXBContext.newInstance(TokenModifyRequest.class).createMarshaller();
-            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
-            unmarshaller = JAXBContext.newInstance(TokenModifyRequest.class).createUnmarshaller();
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    }
-
-    String id;
-    String userID;
-
-    Link link;
-
-    @XmlAttribute(name="id")
-    public String getID() {
-        return id;
-    }
-
-    public void setID(String id) {
-        this.id = id;
-    }
-
-    @XmlElement(name="UserID")
-    public String getUserID() {
-        return userID;
-    }
-
-    public void setUserID(String userID) {
-        this.userID = userID;
-    }
-
-    @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());
-        result = prime * result + ((link == null) ? 0 : link.hashCode());
-        result = prime * result + ((userID == null) ? 0 : userID.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;
-        TokenModifyRequest other = (TokenModifyRequest) obj;
-        if (id == null) {
-            if (other.id != null)
-                return false;
-        } else if (!id.equals(other.id))
-            return false;
-        if (link == null) {
-            if (other.link != null)
-                return false;
-        } else if (!link.equals(other.link))
-            return false;
-        if (userID == null) {
-            if (other.userID != null)
-                return false;
-        } else if (!userID.equals(other.userID))
-            return false;
-        return true;
-    }
-
-    public String toString() {
-        try {
-            StringWriter sw = new StringWriter();
-            marshaller.marshal(this, sw);
-            return sw.toString();
-
-        } catch (Exception e) {
-            return super.toString();
-        }
-    }
-
-    public static TokenModifyRequest valueOf(String string) throws Exception {
-        try {
-            return (TokenModifyRequest)unmarshaller.unmarshal(new StringReader(string));
-        } catch (Exception e) {
-            return null;
-        }
-    }
-
-    public static void main(String args[]) throws Exception {
-
-        TokenModifyRequest before = new TokenModifyRequest();
-        before.setID("token1");
-        before.setUserID("user1");
-
-        String string = before.toString();
-        System.out.println(string);
-
-        TokenModifyRequest after = TokenModifyRequest.valueOf(string);
-        System.out.println(before.equals(after));
-    }
-}
diff --git a/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java b/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
index 0008536233e5dabed21d01b836c5027df7dcb035..529efe8c0c03ae01645110e5f364b73e5e6e1332 100644
--- a/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
+++ b/base/common/src/com/netscape/certsrv/tps/token/TokenResource.java
@@ -33,6 +33,7 @@ import org.jboss.resteasy.annotations.ClientResponseType;
 
 import com.netscape.certsrv.acls.ACLMapping;
 import com.netscape.certsrv.authentication.AuthMethodMapping;
+import com.netscape.certsrv.base.PATCH;
 
 
 /**
@@ -67,11 +68,11 @@ public interface TokenResource {
     @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
     @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
     @ACLMapping("tokens.modify")
-    public Response updateToken(
+    public Response replaceToken(
             @PathParam("tokenID") String tokenID,
             TokenData tokenData);
 
-    @POST
+    @PATCH
     @Path("{tokenID}")
     @ClientResponseType(entityType=TokenData.class)
     @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@@ -79,7 +80,7 @@ public interface TokenResource {
     @ACLMapping("tokens.modify")
     public Response modifyToken(
             @PathParam("tokenID") String tokenID,
-            TokenModifyRequest request);
+            TokenData tokenData);
 
     @DELETE
     @Path("{tokenID}")
diff --git a/base/common/src/com/netscape/certsrv/user/UserResource.java b/base/common/src/com/netscape/certsrv/user/UserResource.java
index 19db939ddf282e1c3415fc19848304ea6910948d..90b46ef607a0ee9ac69c2a340d882201c1d19745 100644
--- a/base/common/src/com/netscape/certsrv/user/UserResource.java
+++ b/base/common/src/com/netscape/certsrv/user/UserResource.java
@@ -22,6 +22,7 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -33,6 +34,7 @@ import org.jboss.resteasy.annotations.ClientResponseType;
 
 import com.netscape.certsrv.acls.ACLMapping;
 import com.netscape.certsrv.authentication.AuthMethodMapping;
+import com.netscape.certsrv.base.PATCH;
 
 /**
  * @author Endi S. Dewata
@@ -63,7 +65,14 @@ public interface UserResource {
     @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
     public UserData getUser(@PathParam("userID") String userID);
 
-    @POST
+    @PUT
+    @Path("{userID}")
+    @ClientResponseType(entityType=UserData.class)
+    @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
+    public Response replaceUser(@PathParam("userID") String userID, UserData userData);
+
+    @PATCH
     @Path("{userID}")
     @ClientResponseType(entityType=UserData.class)
     @Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
diff --git a/base/server/cms/src/com/netscape/cms/servlet/admin/GroupService.java b/base/server/cms/src/com/netscape/cms/servlet/admin/GroupService.java
index 5f9a3b94710bc3c5a00a1c68a9d54aa80b798666..a20b548adc9cad126fbf9ad1991f21b4e0bb8a15 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/admin/GroupService.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/admin/GroupService.java
@@ -267,7 +267,10 @@ public class GroupService extends PKIService implements GroupResource {
                 throw new ResourceNotFoundException("Group " + groupID + "  not found.");
             }
 
-            group.set("description", groupData.getDescription());
+            String description = groupData.getDescription();
+            if (description != null) {
+                group.set("description", description);
+            }
 
             // allow adding a group with no members, except "Certificate
             // Server Administrators"
diff --git a/base/server/cms/src/com/netscape/cms/servlet/admin/UserService.java b/base/server/cms/src/com/netscape/cms/servlet/admin/UserService.java
index c241540946457424c392ad5cf391917721a8a8f6..63a4613555a48faa916a194c6d77d6befe9ad28c 100644
--- a/base/server/cms/src/com/netscape/cms/servlet/admin/UserService.java
+++ b/base/server/cms/src/com/netscape/cms/servlet/admin/UserService.java
@@ -367,6 +367,85 @@ public class UserService extends PKIService implements UserResource {
     }
 
     /**
+     * Replaces an existing user in local scope.
+     * <P>
+     *
+     * Request/Response Syntax: http://warp.mcom.com/server/certificate/columbo/design/
+     * ui/admin-protocol-definition.html#user-admin
+     * <P>
+     *
+     * <ul>
+     * <li>signed.audit LOGGING_SIGNED_AUDIT_CONFIG_ROLE used when configuring role information (anything under
+     * users/groups)
+     * </ul>
+     */
+    @Override
+    public Response replaceUser(String userID, UserData userData) {
+
+        if (userData == null) throw new BadRequestException("User data is null.");
+
+        // ensure that any low-level exceptions are reported
+        // to the signed audit log and stored as failures
+        IConfigStore cs = CMS.getConfigStore();
+        try {
+            if (userID == null) {
+                log(ILogger.LL_FAILURE, CMS.getLogMessage("ADMIN_SRVLT_NULL_RS_ID"));
+                throw new BadRequestException(getUserMessage("CMS_ADMIN_SRVLT_NULL_RS_ID", headers));
+            }
+
+            IUser user = userGroupManager.createUser(userID);
+
+            String fullName = userData.getFullName();
+            user.setFullName(fullName);
+
+            String email = userData.getEmail();
+            user.setEmail(email);
+
+            String pword = userData.getPassword();
+            IPasswordCheck passwdCheck = CMS.getPasswordChecker();
+
+            if (!passwdCheck.isGoodPassword(pword)) {
+                throw new EUsrGrpException(passwdCheck.getReason(pword));
+            }
+
+            user.setPassword(pword);
+
+            String phone = userData.getPhone();
+            user.setPhone(phone);
+
+            String state = userData.getState();
+            user.setState(state);
+
+            String csType = cs.getString("cs.type");
+            if (csType.equals("TPS")) {
+                String tpsProfiles = userData.getAttribute(ATTR_TPS_PROFILES);
+                String[] profiles = tpsProfiles.split(",");
+                user.setTpsProfiles(Arrays.asList(profiles));
+            }
+
+            userGroupManager.modifyUser(user);
+
+            auditModifyUser(userID, userData, ILogger.SUCCESS);
+
+            // read the data back
+            userData = getUser(userID);
+
+            return Response
+                    .ok(userData)
+                    .type(MediaType.APPLICATION_XML)
+                    .build();
+
+        } catch (PKIException e) {
+            auditModifyUser(userID, userData, ILogger.FAILURE);
+            throw e;
+
+        } catch (EBaseException e) {
+            auditModifyUser(userID, userData, ILogger.FAILURE);
+            throw new PKIException(e.getMessage());
+        }
+    }
+
+    /**
      * Modifies an existing user in local scope.
      * <P>
      *
diff --git a/base/tps-tomcat/src/org/dogtagpki/server/tps/token/TokenService.java b/base/tps-tomcat/src/org/dogtagpki/server/tps/token/TokenService.java
index 7991c8c2069e0543703d113ed1f814333399da73..4183ff6e247660fbe89356b5da63742de1f3defc 100644
--- a/base/tps-tomcat/src/org/dogtagpki/server/tps/token/TokenService.java
+++ b/base/tps-tomcat/src/org/dogtagpki/server/tps/token/TokenService.java
@@ -39,7 +39,6 @@ import com.netscape.certsrv.base.BadRequestException;
 import com.netscape.certsrv.base.PKIException;
 import com.netscape.certsrv.tps.token.TokenCollection;
 import com.netscape.certsrv.tps.token.TokenData;
-import com.netscape.certsrv.tps.token.TokenModifyRequest;
 import com.netscape.certsrv.tps.token.TokenResource;
 import com.netscape.cms.servlet.base.PKIService;
 
@@ -200,12 +199,12 @@ public class TokenService extends PKIService implements TokenResource {
     }
 
     @Override
-    public Response updateToken(String tokenID, TokenData tokenData) {
+    public Response replaceToken(String tokenID, TokenData tokenData) {
 
         if (tokenID == null) throw new BadRequestException("Token ID is null.");
         if (tokenData == null) throw new BadRequestException("Token data is null.");
 
-        CMS.debug("TokenService.updateToken(\"" + tokenID + "\")");
+        CMS.debug("TokenService.replaceToken(\"" + tokenID + "\")");
 
         try {
             TPSSubsystem subsystem = (TPSSubsystem)CMS.getSubsystem(TPSSubsystem.ID);
@@ -229,21 +228,27 @@ public class TokenService extends PKIService implements TokenResource {
     }
 
     @Override
-    public Response modifyToken(String tokenID, TokenModifyRequest request) {
+    public Response modifyToken(String tokenID, TokenData tokenData) {
 
         if (tokenID == null) throw new BadRequestException("Token ID is null.");
-        if (request == null) throw new BadRequestException("Token modification is null.");
+        if (tokenData == null) throw new BadRequestException("Token data is null.");
 
-        CMS.debug("TokenService.modifyToken(\"" + tokenID + "\", request");
+        CMS.debug("TokenService.modifyToken(\"" + tokenID + "\")");
 
         try {
             TPSSubsystem subsystem = (TPSSubsystem)CMS.getSubsystem(TPSSubsystem.ID);
             TokenDatabase database = subsystem.getTokenDatabase();
 
             TokenRecord tokenRecord = database.getRecord(tokenID);
-            // TODO: perform modification
 
-            TokenData tokenData = createTokenData(tokenRecord);
+            String userID = tokenData.getUserID();
+            if (userID != null) {
+                tokenRecord.setUserID(userID);
+            }
+
+            database.updateRecord(tokenID, tokenRecord);
+
+            tokenData = createTokenData(database.getRecord(tokenID));
 
             return Response
                     .ok(tokenData)
-- 
1.8.4.2

