Check delete change with PermissionBackend

Deleting the entire change is slightly differently from deleting a
draft patch set.  Update only the change deletion path.

Update test assertions to expect the new generic messages thrown
by the PermissionBackend.

Change-Id: If5a83e386304d8f84db37f6c02f9c35cd5ae4266
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 690c3dd2..d9aeb51 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -557,11 +557,10 @@
     PushOneCommit.Result changeResult =
         pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
 
     setApiUser(user);
     exception.expect(AuthException.class);
-    exception.expectMessage(String.format("Deleting change %s is not permitted", id));
+    exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
   }
 
@@ -605,11 +604,10 @@
     try {
       PushOneCommit.Result changeResult = createChange();
       String changeId = changeResult.getChangeId();
-      Change.Id id = changeResult.getChange().getId();
 
       setApiUser(user);
       exception.expect(AuthException.class);
-      exception.expectMessage(String.format("Deleting change %s is not permitted", id));
+      exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
     } finally {
       removePermission(Permission.DELETE_OWN_CHANGES, project, "refs/*");
@@ -633,13 +631,12 @@
     PushOneCommit.Result changeResult =
         pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
     String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
 
     setApiUser(user);
     gApi.changes().id(changeId).abandon();
 
     exception.expect(AuthException.class);
-    exception.expectMessage(String.format("Deleting change %s is not permitted", id));
+    exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
   }
 
@@ -661,12 +658,11 @@
   public void deleteMergedChange() throws Exception {
     PushOneCommit.Result changeResult = createChange();
     String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
 
     merge(changeResult);
 
     exception.expect(MethodNotAllowedException.class);
-    exception.expectMessage(String.format("Deleting merged change %s is not allowed", id));
+    exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
   }
 
@@ -679,13 +675,12 @@
       PushOneCommit.Result changeResult =
           pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master");
       String changeId = changeResult.getChangeId();
-      Change.Id id = changeResult.getChange().getId();
 
       merge(changeResult);
 
       setApiUser(user);
       exception.expect(MethodNotAllowedException.class);
-      exception.expectMessage(String.format("Deleting merged change %s is not allowed", id));
+      exception.expectMessage("delete not permitted");
       gApi.changes().id(changeId).delete();
     } finally {
       removePermission(Permission.DELETE_OWN_CHANGES, project, "refs/*");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index da7d7b5..26e6847 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -82,14 +82,13 @@
     PushOneCommit.Result changeResult = createDraftChange();
     changeResult.assertOkStatus();
     String changeId = changeResult.getChangeId();
-    Change.Id id = changeResult.getChange().getId();
 
     // The user needs to be able to see the draft change (which reviewers can).
     gApi.changes().id(changeId).addReviewer(user.fullName);
 
     setApiUser(user);
     exception.expect(AuthException.class);
-    exception.expectMessage(String.format("Deleting change %s is not permitted", id));
+    exception.expectMessage("delete not permitted");
     gApi.changes().id(changeId).delete();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index cee2403..e41377e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -375,7 +375,7 @@
   public void delete() throws RestApiException {
     try {
       deleteChange.apply(change, null);
-    } catch (UpdateException e) {
+    } catch (UpdateException | PermissionBackendException e) {
       throw new RestApiException("Cannot delete change", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index d934f6c..6440509 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -262,7 +262,7 @@
   public void delete() throws RestApiException {
     try {
       deleteDraft.apply(revision, null);
-    } catch (UpdateException e) {
+    } catch (UpdateException | OrmException | PermissionBackendException e) {
       throw new RestApiException("Cannot delete draft ps", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
index ad823d4..151ffa1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -15,20 +15,24 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.change.DeleteChange.Input;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.Order;
 import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -42,6 +46,8 @@
   private final Provider<ReviewDb> db;
   private final BatchUpdate.Factory updateFactory;
   private final Provider<DeleteChangeOp> opProvider;
+  private final Provider<CurrentUser> user;
+  private final PermissionBackend permissionBackend;
   private final boolean allowDrafts;
 
   @Inject
@@ -49,16 +55,33 @@
       Provider<ReviewDb> db,
       BatchUpdate.Factory updateFactory,
       Provider<DeleteChangeOp> opProvider,
+      Provider<CurrentUser> user,
+      PermissionBackend permissionBackend,
       @GerritServerConfig Config cfg) {
     this.db = db;
     this.updateFactory = updateFactory;
     this.opProvider = opProvider;
+    this.user = user;
+    this.permissionBackend = permissionBackend;
     this.allowDrafts = DeleteChangeOp.allowDrafts(cfg);
   }
 
   @Override
   public Response<?> apply(ChangeResource rsrc, Input input)
-      throws RestApiException, UpdateException {
+      throws RestApiException, UpdateException, PermissionBackendException {
+    if (rsrc.getChange().getStatus() == Change.Status.MERGED) {
+      throw new MethodNotAllowedException("delete not permitted");
+    } else if (!allowDrafts && rsrc.getChange().getStatus() == Change.Status.DRAFT) {
+      // If drafts are disabled, only an administrator can delete a draft.
+      try {
+        permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+      } catch (AuthException e) {
+        throw new MethodNotAllowedException("Draft workflow is disabled");
+      }
+    } else {
+      rsrc.permissions().database(db).check(ChangePermission.DELETE);
+    }
+
     try (BatchUpdate bu =
         updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       Change.Id id = rsrc.getChange().getId();
@@ -71,21 +94,33 @@
 
   @Override
   public UiAction.Description getDescription(ChangeResource rsrc) {
-    try {
-      Change.Status status = rsrc.getChange().getStatus();
-      ChangeControl changeControl = rsrc.getControl();
-      boolean visible =
-          isActionAllowed(changeControl, status) && changeControl.canDelete(db.get(), status);
-      return new UiAction.Description()
-          .setLabel("Delete")
-          .setTitle("Delete change " + rsrc.getId())
-          .setVisible(visible);
-    } catch (OrmException e) {
-      throw new IllegalStateException(e);
-    }
+    Change.Status status = rsrc.getChange().getStatus();
+    PermissionBackend.ForChange perm = rsrc.permissions().database(db);
+    return new UiAction.Description()
+        .setLabel("Delete")
+        .setTitle("Delete change " + rsrc.getId())
+        .setVisible(couldDeleteWhenIn(status) && perm.testOrFalse(ChangePermission.DELETE));
   }
 
-  private boolean isActionAllowed(ChangeControl changeControl, Status status) {
-    return status != Status.DRAFT || allowDrafts || changeControl.isAdmin();
+  private boolean couldDeleteWhenIn(Change.Status status) {
+    switch (status) {
+      case NEW:
+      case ABANDONED:
+        // New or abandoned changes can be deleted with the right permissions.
+        return true;
+
+      case MERGED:
+        // Merged changes should never be deleted.
+        return false;
+
+      case DRAFT:
+        if (allowDrafts) {
+          // Drafts can only be deleted if the server has drafts enabled.
+          return true;
+        }
+        // If drafts are disabled, only administrators may delete.
+        return permissionBackend.user(user).testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
+    }
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 992313d..8ab1d2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -17,7 +17,6 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
-import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
@@ -27,7 +26,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.StarredChangesUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.BatchUpdateReviewDb;
@@ -62,7 +60,6 @@
   private final PatchSetUtil psUtil;
   private final StarredChangesUtil starredChangesUtil;
   private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
-  private final boolean allowDrafts;
 
   private Change.Id id;
 
@@ -70,12 +67,10 @@
   DeleteChangeOp(
       PatchSetUtil psUtil,
       StarredChangesUtil starredChangesUtil,
-      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore,
-      @GerritServerConfig Config cfg) {
+      DynamicItem<AccountPatchReviewStore> accountPatchReviewStore) {
     this.psUtil = psUtil;
     this.starredChangesUtil = starredChangesUtil;
     this.accountPatchReviewStore = accountPatchReviewStore;
-    this.allowDrafts = allowDrafts(cfg);
   }
 
   @Override
@@ -99,8 +94,7 @@
   }
 
   private void ensureDeletable(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets)
-      throws ResourceConflictException, MethodNotAllowedException, OrmException, AuthException,
-          IOException {
+      throws ResourceConflictException, MethodNotAllowedException, IOException {
     Change.Status status = ctx.getChange().getStatus();
     if (status == Change.Status.MERGED) {
       throw new MethodNotAllowedException("Deleting merged change " + id + " is not allowed");
@@ -114,14 +108,7 @@
       }
     }
 
-    if (!ctx.getControl().canDelete(ctx.getDb(), status)) {
-      throw new AuthException("Deleting change " + id + " is not permitted");
-    }
-
     if (status == Change.Status.DRAFT) {
-      if (!allowDrafts && !ctx.getControl().isAdmin()) {
-        throw new MethodNotAllowedException("Draft workflow is disabled");
-      }
       for (PatchSet ps : patchSets) {
         if (!ps.isDraft()) {
           throw new ResourceConflictException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
index a4db8c5..583bc58 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -32,6 +33,8 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
@@ -81,7 +84,12 @@
 
   @Override
   public Response<?> apply(RevisionResource rsrc, Input input)
-      throws RestApiException, UpdateException {
+      throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+    if (isDeletingOnlyPatchSet(rsrc)) {
+      // A change cannot have zero patch sets; the change is deleted instead.
+      rsrc.permissions().database(db).check(ChangePermission.DELETE);
+    }
+
     try (BatchUpdate bu =
         updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
       bu.setOrder(Order.DB_BEFORE_REPO);
@@ -91,6 +99,12 @@
     return Response.none();
   }
 
+  private boolean isDeletingOnlyPatchSet(RevisionResource rsrc) throws OrmException {
+    Collection<PatchSet> patchSets = psUtil.byChange(db.get(), rsrc.getNotes());
+    return patchSets.size() == 1
+        && Iterables.getOnlyElement(patchSets).getId().equals(rsrc.getPatchSet().getId());
+  }
+
   private class Op implements BatchUpdateOp {
     private final PatchSet.Id psId;
 
@@ -183,15 +197,14 @@
   @Override
   public UiAction.Description getDescription(RevisionResource rsrc) {
     try {
-      int psCount = psUtil.byChange(db.get(), rsrc.getNotes()).size();
       return new UiAction.Description()
           .setLabel("Delete")
           .setTitle(String.format("Delete draft revision %d", rsrc.getPatchSet().getPatchSetId()))
           .setVisible(
               allowDrafts
                   && rsrc.getPatchSet().isDraft()
-                  && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT)
-                  && psCount > 1);
+                  && psUtil.byChange(db.get(), rsrc.getNotes()).size() > 1
+                  && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT));
     } catch (OrmException e) {
       throw new IllegalStateException(e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3e5eba3..1bb1103 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -275,7 +275,7 @@
 
     switch (status) {
       case DRAFT:
-        return (isOwner() || getRefControl().canDeleteDrafts());
+        return isOwner() || getRefControl().canDeleteDrafts() || isAdmin();
       case NEW:
       case ABANDONED:
         return (isAdmin() || (isOwner() && getRefControl().canDeleteOwnChanges()));