Move Rebase#onBehalfOf method into RebaseUtil

We want to support rebase on behalf of the uploader also for the
RebaseChain REST endpoint. As a first step towards this move the
Rebase#onBehalfOf method into RebaseUtil so it can be reused in
RebaseChain.

Release-Notes: skip
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: Ie3c25f2bc73526331853a687711a647259096252
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index 8acc925..65fef05 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -18,29 +18,38 @@
 import com.google.common.flogger.FluentLogger;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.BranchNameKey;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.git.ObjectIds;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -50,6 +59,11 @@
 public class RebaseUtil {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final Provider<PersonIdent> serverIdent;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final PermissionBackend permissionBackend;
+  private final ChangeResource.Factory changeResourceFactory;
+  private final GitRepositoryManager repoManager;
   private final Provider<InternalChangeQuery> queryProvider;
   private final ChangeNotes.Factory notesFactory;
   private final PatchSetUtil psUtil;
@@ -57,10 +71,20 @@
 
   @Inject
   RebaseUtil(
+      @GerritPersonIdent Provider<PersonIdent> serverIdent,
+      IdentifiedUser.GenericFactory userFactory,
+      PermissionBackend permissionBackend,
+      ChangeResource.Factory changeResourceFactory,
+      GitRepositoryManager repoManager,
       Provider<InternalChangeQuery> queryProvider,
       ChangeNotes.Factory notesFactory,
       PatchSetUtil psUtil,
       RebaseChangeOp.Factory rebaseFactory) {
+    this.serverIdent = serverIdent;
+    this.userFactory = userFactory;
+    this.permissionBackend = permissionBackend;
+    this.changeResourceFactory = changeResourceFactory;
+    this.repoManager = repoManager;
     this.queryProvider = queryProvider;
     this.notesFactory = notesFactory;
     this.psUtil = psUtil;
@@ -68,6 +92,143 @@
   }
 
   /**
+   * Checks that the uploader has permissions to create a new patch set and creates a new {@link
+   * RevisionResource} that contains the uploader (aka the impersonated user) as the current user
+   * which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
+   *
+   * <p>The following permissions are required for the uploader:
+   *
+   * <ul>
+   *   <li>The {@code Read} permission that allows to see the change.
+   *   <li>The {@code Push} permission that allows upload.
+   *   <li>The {@code Add Patch Set} permission, required if the change is owned by another user
+   *       (change owners implicitly have this permission).
+   *   <li>The {@code Forge Author} permission if the patch set that is rebased has a forged author
+   *       (author != uploader).
+   *   <li>The {@code Forge Server} permission if the patch set that is rebased has the server
+   *       identity as the author.
+   * </ul>
+   *
+   * <p>Usually the uploader should have all these permission since they were already required for
+   * the original upload, but there is the edge case that the uploader had the permission when doing
+   * the original upload and then the permission was revoked.
+   *
+   * <p>Note that patch sets with a forged committer (committer != uploader) can be rebased on
+   * behalf of the uploader, even if the uploader doesn't have the {@code Forge Committer}
+   * permission. This is because on rebase on behalf of the uploader the uploader will become the
+   * committer of the new rebased patch set, hence for the rebased patch set the committer is no
+   * longer forged (committer == uploader) and hence the {@code Forge Committer} permission is not
+   * required.
+   *
+   * <p>Note that the {@code Rebase} permission is not required for the uploader since the {@code
+   * Rebase} permission is specifically about allowing a user to do a rebase via the web UI by
+   * clicking on the {@code REBASE} button and the uploader is not clicking on this button.
+   *
+   * <p>The permissions of the uploader are checked explicitly here so that we can return a {@code
+   * 409 Conflict} response with a proper error message if they are missing (the error message says
+   * that the permission is missing for the uploader). The normal code path also checks these
+   * permission but the exception thrown there would result in a {@code 403 Forbidden} response and
+   * the error message would wrongly look like the caller (i.e. the rebaser) is missing the
+   * permission.
+   *
+   * <p>Note that this method doesn't check permissions for the rebaser (aka the impersonating user
+   * aka the calling user). Callers should check the permissions for the rebaser before calling this
+   * method.
+   *
+   * @param rsrc the revision resource that should be rebased
+   * @param rebaseInput the request input containing options for the rebase
+   * @return revision resource that contains the uploader (aka the impersonated user) as the current
+   *     user which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader
+   */
+  public RevisionResource onBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
+      throws IOException, PermissionBackendException, BadRequestException,
+          ResourceConflictException {
+    if (rsrc.getPatchSet().id().get() != rsrc.getChange().currentPatchSetId().get()) {
+      throw new BadRequestException(
+          "non-current patch set cannot be rebased on behalf of the uploader");
+    }
+    if (rebaseInput.allowConflicts) {
+      throw new BadRequestException(
+          "allow_conflicts and on_behalf_of_uploader are mutually exclusive");
+    }
+
+    CurrentUser caller = rsrc.getUser();
+    Account.Id uploaderId = rsrc.getPatchSet().uploader();
+    IdentifiedUser uploader = userFactory.runAs(/*remotePeer= */ null, uploaderId, caller);
+    logger.atFine().log(
+        "%s is rebasing patch set %s of project %s on behalf of uploader %s",
+        caller.getLoggableName(),
+        rsrc.getPatchSet().id(),
+        rsrc.getProject(),
+        uploader.getLoggableName());
+
+    checkPermissionForUploader(
+        uploader,
+        rsrc.getNotes(),
+        ChangePermission.READ,
+        String.format("uploader %s cannot read change", uploader.getLoggableName()));
+    checkPermissionForUploader(
+        uploader,
+        rsrc.getNotes(),
+        ChangePermission.ADD_PATCH_SET,
+        String.format("uploader %s cannot add patch set", uploader.getLoggableName()));
+
+    try (Repository repo = repoManager.openRepository(rsrc.getProject())) {
+      RevCommit commit = repo.parseCommit(rsrc.getPatchSet().commitId());
+
+      if (!uploader.hasEmailAddress(commit.getAuthorIdent().getEmailAddress())) {
+        checkPermissionForUploader(
+            uploader,
+            rsrc.getNotes(),
+            RefPermission.FORGE_AUTHOR,
+            String.format(
+                "author of patch set %d is forged and the uploader %s cannot forge author",
+                rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
+
+        if (serverIdent.get().getEmailAddress().equals(commit.getAuthorIdent().getEmailAddress())) {
+          checkPermissionForUploader(
+              uploader,
+              rsrc.getNotes(),
+              RefPermission.FORGE_SERVER,
+              String.format(
+                  "author of patch set %d is the server identity and the uploader %s cannot forge"
+                      + " the server identity",
+                  rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
+        }
+      }
+    }
+
+    return new RevisionResource(
+        changeResourceFactory.create(rsrc.getNotes(), uploader), rsrc.getPatchSet());
+  }
+
+  private void checkPermissionForUploader(
+      IdentifiedUser uploader,
+      ChangeNotes changeNotes,
+      ChangePermission changePermission,
+      String errorMessage)
+      throws PermissionBackendException, ResourceConflictException {
+    try {
+      permissionBackend.user(uploader).change(changeNotes).check(changePermission);
+    } catch (AuthException e) {
+      throw new ResourceConflictException(errorMessage, e);
+    }
+  }
+
+  private void checkPermissionForUploader(
+      IdentifiedUser uploader,
+      ChangeNotes changeNotes,
+      RefPermission refPermission,
+      String errorMessage)
+      throws PermissionBackendException, ResourceConflictException {
+    try {
+      permissionBackend.user(uploader).ref(changeNotes.getChange().getDest()).check(refPermission);
+    } catch (AuthException e) {
+      throw new ResourceConflictException(errorMessage, e);
+    }
+  }
+
+  /**
    * Checks whether the given change fulfills all preconditions to be rebased.
    *
    * <p>This method does not check whether the calling user is allowed to rebase the change.
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 5368c75..3cb1870 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -19,23 +19,16 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.extensions.api.changes.RebaseInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.change.ChangeJson;
 import com.google.gerrit.server.change.ChangeResource;
@@ -45,36 +38,28 @@
 import com.google.gerrit.server.change.RevisionResource;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.UpdateException;
 import com.google.gerrit.server.update.context.RefUpdateContext;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
 public class Rebase
     implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   private static final ImmutableSet<ListChangesOption> OPTIONS =
       Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
 
-  private final Provider<PersonIdent> serverIdent;
   private final BatchUpdate.Factory updateFactory;
   private final GitRepositoryManager repoManager;
   private final RebaseUtil rebaseUtil;
@@ -82,13 +67,10 @@
   private final PermissionBackend permissionBackend;
   private final ProjectCache projectCache;
   private final PatchSetUtil patchSetUtil;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final ChangeResource.Factory changeResourceFactory;
   private final RebaseMetrics rebaseMetrics;
 
   @Inject
   public Rebase(
-      @GerritPersonIdent Provider<PersonIdent> serverIdent,
       BatchUpdate.Factory updateFactory,
       GitRepositoryManager repoManager,
       RebaseUtil rebaseUtil,
@@ -96,10 +78,7 @@
       PermissionBackend permissionBackend,
       ProjectCache projectCache,
       PatchSetUtil patchSetUtil,
-      IdentifiedUser.GenericFactory userFactory,
-      ChangeResource.Factory changeResourceFactory,
       RebaseMetrics rebaseMetrics) {
-    this.serverIdent = serverIdent;
     this.updateFactory = updateFactory;
     this.repoManager = repoManager;
     this.rebaseUtil = rebaseUtil;
@@ -107,8 +86,6 @@
     this.permissionBackend = permissionBackend;
     this.projectCache = projectCache;
     this.patchSetUtil = patchSetUtil;
-    this.userFactory = userFactory;
-    this.changeResourceFactory = changeResourceFactory;
     this.rebaseMetrics = rebaseMetrics;
   }
 
@@ -118,7 +95,7 @@
 
     if (input.onBehalfOfUploader && !rsrc.getPatchSet().uploader().equals(rsrc.getAccountId())) {
       rsrc.permissions().check(ChangePermission.REBASE_ON_BEHALF_OF_UPLOADER);
-      rsrc = onBehalfOf(rsrc, input);
+      rsrc = rebaseUtil.onBehalfOf(rsrc, input);
     } else {
       rsrc.permissions().check(ChangePermission.REBASE);
     }
@@ -160,143 +137,6 @@
     }
   }
 
-  /**
-   * Checks that the uploader has permissions to create a new patch set and creates a new {@link
-   * RevisionResource} that contains the uploader (aka the impersonated user) as the current user
-   * which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader.
-   *
-   * <p>The following permissions are required for the uploader:
-   *
-   * <ul>
-   *   <li>The {@code Read} permission that allows to see the change.
-   *   <li>The {@code Push} permission that allows upload.
-   *   <li>The {@code Add Patch Set} permission, required if the change is owned by another user
-   *       (change owners implicitly have this permission).
-   *   <li>The {@code Forge Author} permission if the patch set that is rebased has a forged author
-   *       (author != uploader).
-   *   <li>The {@code Forge Server} permission if the patch set that is rebased has the server
-   *       identity as the author.
-   * </ul>
-   *
-   * <p>Usually the uploader should have all these permission since they were already required for
-   * the original upload, but there is the edge case that the uploader had the permission when doing
-   * the original upload and then the permission was revoked.
-   *
-   * <p>Note that patch sets with a forged committer (committer != uploader) can be rebased on
-   * behalf of the uploader, even if the uploader doesn't have the {@code Forge Committer}
-   * permission. This is because on rebase on behalf of the uploader the uploader will become the
-   * committer of the new rebased patch set, hence for the rebased patch set the committer is no
-   * longer forged (committer == uploader) and hence the {@code Forge Committer} permission is not
-   * required.
-   *
-   * <p>Note that the {@code Rebase} permission is not required for the uploader since the {@code
-   * Rebase} permission is specifically about allowing a user to do a rebase via the web UI by
-   * clicking on the {@code REBASE} button and the uploader is not clicking on this button.
-   *
-   * <p>The permissions of the uploader are checked explicitly here so that we can return a {@code
-   * 409 Conflict} response with a proper error message if they are missing (the error message says
-   * that the permission is missing for the uploader). The normal code path also checks these
-   * permission but the exception thrown there would result in a {@code 403 Forbidden} response and
-   * the error message would wrongly look like the caller (i.e. the rebaser) is missing the
-   * permission.
-   *
-   * <p>Note that this method doesn't check permissions for the rebaser (aka the impersonating user
-   * aka the calling user). Callers should check the permissions for the rebaser before calling this
-   * method.
-   *
-   * @param rsrc the revision resource that should be rebased
-   * @param rebaseInput the request input containing options for the rebase
-   * @return revision resource that contains the uploader (aka the impersonated user) as the current
-   *     user which can be used for {@link BatchUpdate} to do the rebase on behalf of the uploader
-   */
-  private RevisionResource onBehalfOf(RevisionResource rsrc, RebaseInput rebaseInput)
-      throws IOException, PermissionBackendException, BadRequestException,
-          ResourceConflictException {
-    if (rsrc.getPatchSet().id().get() != rsrc.getChange().currentPatchSetId().get()) {
-      throw new BadRequestException(
-          "non-current patch set cannot be rebased on behalf of the uploader");
-    }
-    if (rebaseInput.allowConflicts) {
-      throw new BadRequestException(
-          "allow_conflicts and on_behalf_of_uploader are mutually exclusive");
-    }
-
-    CurrentUser caller = rsrc.getUser();
-    Account.Id uploaderId = rsrc.getPatchSet().uploader();
-    IdentifiedUser uploader = userFactory.runAs(/*remotePeer= */ null, uploaderId, caller);
-    logger.atFine().log(
-        "%s is rebasing patch set %s of project %s on behalf of uploader %s",
-        caller.getLoggableName(),
-        rsrc.getPatchSet().id(),
-        rsrc.getProject(),
-        uploader.getLoggableName());
-
-    checkPermissionForUploader(
-        uploader,
-        rsrc.getNotes(),
-        ChangePermission.READ,
-        String.format("uploader %s cannot read change", uploader.getLoggableName()));
-    checkPermissionForUploader(
-        uploader,
-        rsrc.getNotes(),
-        ChangePermission.ADD_PATCH_SET,
-        String.format("uploader %s cannot add patch set", uploader.getLoggableName()));
-
-    try (Repository repo = repoManager.openRepository(rsrc.getProject())) {
-      RevCommit commit = repo.parseCommit(rsrc.getPatchSet().commitId());
-
-      if (!uploader.hasEmailAddress(commit.getAuthorIdent().getEmailAddress())) {
-        checkPermissionForUploader(
-            uploader,
-            rsrc.getNotes(),
-            RefPermission.FORGE_AUTHOR,
-            String.format(
-                "author of patch set %d is forged and the uploader %s cannot forge author",
-                rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
-
-        if (serverIdent.get().getEmailAddress().equals(commit.getAuthorIdent().getEmailAddress())) {
-          checkPermissionForUploader(
-              uploader,
-              rsrc.getNotes(),
-              RefPermission.FORGE_SERVER,
-              String.format(
-                  "author of patch set %d is the server identity and the uploader %s cannot forge"
-                      + " the server identity",
-                  rsrc.getPatchSet().id().get(), uploader.getLoggableName()));
-        }
-      }
-    }
-
-    return new RevisionResource(
-        changeResourceFactory.create(rsrc.getNotes(), uploader), rsrc.getPatchSet());
-  }
-
-  private void checkPermissionForUploader(
-      IdentifiedUser uploader,
-      ChangeNotes changeNotes,
-      ChangePermission changePermission,
-      String errorMessage)
-      throws PermissionBackendException, ResourceConflictException {
-    try {
-      permissionBackend.user(uploader).change(changeNotes).check(changePermission);
-    } catch (AuthException e) {
-      throw new ResourceConflictException(errorMessage, e);
-    }
-  }
-
-  private void checkPermissionForUploader(
-      IdentifiedUser uploader,
-      ChangeNotes changeNotes,
-      RefPermission refPermission,
-      String errorMessage)
-      throws PermissionBackendException, ResourceConflictException {
-    try {
-      permissionBackend.user(uploader).ref(changeNotes.getChange().getDest()).check(refPermission);
-    } catch (AuthException e) {
-      throw new ResourceConflictException(errorMessage, e);
-    }
-  }
-
   @Override
   public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
     UiAction.Description description =