Add global capability that allows the deletion of own uploaded images

After the upload of an image a user may relalize that he did a mistake
and uploaded a wrong image. In this case the user wants to delete the
uploaded image, but administrators don't want to allow users to delete
arbitary uploaded images. This is way it makes sense to have a global
capability which allows the deletion of own uploaded images without
need to have force push on the images namespace which would allow the
user to delete any uploaded image.

Change-Id: I5c5bdff02c85070d04cf06269ddebe6d6422805f
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteImage.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteImage.java
index 6446e2a..675c99d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteImage.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteImage.java
@@ -15,8 +15,10 @@
 package com.googlesource.gerrit.plugins.imagare;
 
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.IdentifiedUser;
@@ -28,8 +30,12 @@
 import com.googlesource.gerrit.plugins.imagare.DeleteImage.Input;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,15 +47,17 @@
   public static class Input {
   }
 
+  private final String pluginName;
   private final Provider<IdentifiedUser> self;
   private final GitRepositoryManager repoManager;
   private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
 
   @Inject
-  public DeleteImage(Provider<IdentifiedUser> self,
+  public DeleteImage(@PluginName String pluginName, Provider<IdentifiedUser> self,
       GitRepositoryManager repoManager, GitReferenceUpdated referenceUpdated,
       ChangeHooks hooks) {
+    this.pluginName = pluginName;
     this.self = self;
     this.repoManager = repoManager;
     this.referenceUpdated = referenceUpdated;
@@ -59,13 +67,19 @@
   @Override
   public Response<?> apply(ImageResource rsrc, Input input)
       throws AuthException, ResourceConflictException,
-      RepositoryNotFoundException, IOException {
-    if (!rsrc.getControl().canDelete()) {
+      RepositoryNotFoundException, IOException, ResourceNotFoundException {
+    if (!rsrc.getControl().canDelete()
+        && !self.get().getCapabilities()
+            .canPerform(pluginName + "-" + DeleteOwnImagesCapability.DELETE_OWN_IMAGES)) {
       throw new AuthException("not allowed to delete image");
     }
 
     Repository r = repoManager.openRepository(rsrc.getProject());
     try {
+      if (!rsrc.getControl().canDelete()) {
+        validateOwnImage(r, rsrc.getRef());
+      }
+
       RefUpdate.Result result;
       RefUpdate u;
       try {
@@ -99,4 +113,26 @@
     }
     return Response.none();
   }
+
+  private void validateOwnImage(Repository repo, String ref)
+      throws IOException, ResourceNotFoundException, AuthException {
+    Ref r = repo.getRef(ref);
+    if (r == null) {
+      throw new ResourceNotFoundException(ref);
+    }
+    RevWalk rw = new RevWalk(repo);
+    try {
+      RevCommit commit = rw.parseCommit(r.getObjectId());
+      if (!self.get().getNameEmail()
+          .equals(getNameEmail(commit.getCommitterIdent()))) {
+        throw new AuthException("not allowed to delete image");
+      }
+    } finally {
+      rw.release();
+    }
+  }
+
+  private String getNameEmail(PersonIdent ident) {
+    return ident.getName() + " <" + ident.getEmailAddress() + ">";
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteOwnImagesCapability.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteOwnImagesCapability.java
new file mode 100644
index 0000000..2fcc529
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/DeleteOwnImagesCapability.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.imagare;
+
+import com.google.gerrit.extensions.config.CapabilityDefinition;
+
+public class DeleteOwnImagesCapability extends CapabilityDefinition {
+  static final String DELETE_OWN_IMAGES = "deleteOwnImages";
+
+  @Override
+  public String getDescription() {
+    return "Delete Own Images";
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
index 8388c97..309e5fd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/Module.java
@@ -17,8 +17,10 @@
 import static com.google.gerrit.server.config.ConfigResource.CONFIG_KIND;
 import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
 import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
+import static com.googlesource.gerrit.plugins.imagare.DeleteOwnImagesCapability.DELETE_OWN_IMAGES;
 import static com.googlesource.gerrit.plugins.imagare.ImageResource.IMAGE_KIND;
 
+import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.restapi.RestApiModule;
@@ -30,6 +32,9 @@
   @Override
   protected void configure() {
     DynamicSet.bind(binder(), TopMenu.class).to(ImagareMenu.class);
+    bind(com.google.gerrit.extensions.config.CapabilityDefinition.class)
+        .annotatedWith(Exports.named(DELETE_OWN_IMAGES))
+        .to(DeleteOwnImagesCapability.class);
     install(new RestApiModule() {
       @Override
       protected void configure() {
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 93b3846..de447b1 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -25,3 +25,10 @@
 to make images available for browsing. Also the image mime types must be
 [configured as safe](../../../Documentation/config-gerrit.html#mimetype.name.safe)
 because otherwise Gerrit refuses to render the images.
+
+To allow deletions of own uploaded images the global capability
+`Delete Own Images` can be granted.
+
+To allow the deletion of any uploaded image the
+[Force Push](../../../Documentation/access-control.html#category_push)
+access right can be assigned on the images namespace.
diff --git a/src/main/resources/Documentation/rest-api-projects.md b/src/main/resources/Documentation/rest-api-projects.md
index ca76941..83bf26e 100644
--- a/src/main/resources/Documentation/rest-api-projects.md
+++ b/src/main/resources/Documentation/rest-api-projects.md
@@ -46,7 +46,9 @@
 
 Caller must have the
 [Force Push](../../../Documentation/access-control.html#category_push)
-access right on the `refs/images/*` namespace of the project.
+access right on the `refs/images/*` namespace of the project or be
+granted the `Delete Own Images` global capability which allows deleting
+own images.
 
 #### Request