Use credentials of configurable contextUserId for API calls.

Allow setting contextUserId in the config so that all automerger created
CLs and votes can run using the permissions of that user. This will
allow +2s and other things that are normally restricted for a user to
work downstream under the credentials of a robot account.

Not setting contextUserId will mean that a user's credentials will
continue to be the credentials used downstream.

Change-Id: If861a2cffdb68911fb7821c568a3fef67c0d0ddc
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
index f16f023..92d8176 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/AutomergeChangeAction.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
@@ -57,9 +58,12 @@
    * @return HTTP 200 on success.
    * @throws IOException
    * @throws RestApiException
+   * @throws ConfigInvalidException
+   * @throws OrmException
    */
   @Override
-  public Object apply(RevisionResource rev, Input input) throws IOException, RestApiException {
+  public Object apply(RevisionResource rev, Input input)
+      throws IOException, RestApiException, OrmException, ConfigInvalidException {
     Map<String, Boolean> branchMap = input.branchMap;
 
     Change change = rev.getChange();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
index 762e85d..8b66cf3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/ConfigLoader.java
@@ -21,11 +21,14 @@
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.re2j.Pattern;
 import java.io.IOException;
@@ -50,6 +53,7 @@
   private final String canonicalWebUrl;
   private final AllProjectsName allProjectsName;
   private final PluginConfigFactory cfgFactory;
+  private Provider<CurrentUser> user;
 
   /**
    * Class to handle getting information from the config.
@@ -65,12 +69,14 @@
       AllProjectsName allProjectsName,
       @PluginName String pluginName,
       @CanonicalWebUrl String canonicalWebUrl,
-      PluginConfigFactory cfgFactory) {
+      PluginConfigFactory cfgFactory,
+      Provider<CurrentUser> user) {
     this.gApi = gApi;
     this.canonicalWebUrl = canonicalWebUrl;
     this.pluginName = pluginName;
     this.cfgFactory = cfgFactory;
     this.allProjectsName = allProjectsName;
+    this.user = user;
   }
 
   private Config getConfig() throws ConfigInvalidException {
@@ -250,6 +256,14 @@
     return getConfig().getBoolean("global", "disableMinAutomergeVote", false);
   }
 
+  public Account.Id getContextUserId() throws ConfigInvalidException {
+    int contextUserId = getConfig().getInt("global", "contextUserId", -1);
+    if (contextUserId > 0) {
+      return new Account.Id(contextUserId);
+    }
+    return user.get().getAccountId();
+  }
+
   // Returns overriden manifest config if specified, default if not
   private String getManifestFile() throws ConfigInvalidException {
     String manifestFile = getConfig().getString("global", null, "manifestFile");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
index f7469d1..df331ad 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreator.java
@@ -40,6 +40,11 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -75,11 +80,16 @@
 
   protected GerritApi gApi;
   protected ConfigLoader config;
+  protected CurrentUser user;
+
+  private final OneOffRequestContext oneOffRequestContext;
 
   @Inject
-  public DownstreamCreator(GerritApi gApi, ConfigLoader config) {
+  public DownstreamCreator(
+      GerritApi gApi, ConfigLoader config, OneOffRequestContext oneOffRequestContext) {
     this.gApi = gApi;
     this.config = config;
+    this.oneOffRequestContext = oneOffRequestContext;
   }
 
   /**
@@ -89,13 +99,13 @@
    */
   @Override
   public void onChangeAbandoned(ChangeAbandonedListener.Event event) {
-    ChangeInfo change = event.getChange();
-    String revision = event.getRevision().commit.commit;
-    log.debug("Detected revision {} abandoned on {}.", revision, change.project);
-    try {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ChangeInfo change = event.getChange();
+      String revision = event.getRevision().commit.commit;
+      log.debug("Detected revision {} abandoned on {}.", revision, change.project);
       abandonDownstream(change, revision);
-    } catch (ConfigInvalidException e) {
-      log.error("Automerger plugin failed onChangeAbandoned for {}", change.id, e);
+    } catch (ConfigInvalidException | OrmException e) {
+      log.error("Automerger plugin failed onChangeAbandoned for {}", event.getChange().id, e);
     }
   }
 
@@ -106,59 +116,63 @@
    */
   @Override
   public void onTopicEdited(TopicEditedListener.Event event) {
-    ChangeInfo eventChange = event.getChange();
-    // We have to re-query for this in order to include the current revision
-    ChangeInfo change;
-    try {
-      change =
-          gApi.changes()
-              .id(eventChange._number)
-              .get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
-    } catch (RestApiException e) {
-      log.error("Automerger could not get change with current revision for onTopicEdited: ", e);
-      return;
-    }
-    String oldTopic = event.getOldTopic();
-    String revision = change.currentRevision;
-    Set<String> downstreamBranches;
-    try {
-      downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
-    } catch (RestApiException | IOException | ConfigInvalidException e) {
-      log.error("Failed to edit downstream topics of {}", change.id, e);
-      return;
-    }
-
-    if (downstreamBranches.isEmpty()) {
-      log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
-      return;
-    }
-
-    // If change is empty, prevent someone breaking topic.
-    if (isNullOrEmpty(change.topic)) {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ChangeInfo eventChange = event.getChange();
+      // We have to re-query for this in order to include the current revision
+      ChangeInfo change;
       try {
-        gApi.changes().id(change._number).topic(oldTopic);
-        ReviewInput reviewInput = new ReviewInput();
-        reviewInput.message(
-            "Automerger prevented the topic from changing. Topic can only be modified on "
-                + "non-automerger-created CLs to a non-empty value.");
-        reviewInput.notify = NotifyHandling.NONE;
-        gApi.changes().id(change._number).revision(change.currentRevision).review(reviewInput);
+        change =
+            gApi.changes()
+                .id(eventChange._number)
+                .get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
       } catch (RestApiException e) {
-        log.error("Failed to prevent setting empty topic for automerger plugin.", e);
+        log.error("Automerger could not get change with current revision for onTopicEdited: ", e);
+        return;
       }
-    } else {
-      for (String downstreamBranch : downstreamBranches) {
+      String oldTopic = event.getOldTopic();
+      String revision = change.currentRevision;
+      Set<String> downstreamBranches;
+      try {
+        downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
+      } catch (RestApiException | IOException | ConfigInvalidException e) {
+        log.error("Failed to edit downstream topics of {}", change.id, e);
+        return;
+      }
+
+      if (downstreamBranches.isEmpty()) {
+        log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
+        return;
+      }
+
+      // If change is empty, prevent someone breaking topic.
+      if (isNullOrEmpty(change.topic)) {
         try {
-          List<Integer> existingDownstream =
-              getExistingMergesOnBranch(revision, oldTopic, downstreamBranch);
-          for (Integer changeNumber : existingDownstream) {
-            log.debug("Setting topic {} on {}", change.topic, changeNumber);
-            gApi.changes().id(changeNumber).topic(change.topic);
+          gApi.changes().id(change._number).topic(oldTopic);
+          ReviewInput reviewInput = new ReviewInput();
+          reviewInput.message(
+              "Automerger prevented the topic from changing. Topic can only be modified on "
+                  + "non-automerger-created CLs to a non-empty value.");
+          reviewInput.notify = NotifyHandling.NONE;
+          gApi.changes().id(change._number).revision(change.currentRevision).review(reviewInput);
+        } catch (RestApiException e) {
+          log.error("Failed to prevent setting empty topic for automerger plugin.", e);
+        }
+      } else {
+        for (String downstreamBranch : downstreamBranches) {
+          try {
+            List<Integer> existingDownstream =
+                getExistingMergesOnBranch(revision, oldTopic, downstreamBranch);
+            for (Integer changeNumber : existingDownstream) {
+              log.debug("Setting topic {} on {}", change.topic, changeNumber);
+              gApi.changes().id(changeNumber).topic(change.topic);
+            }
+          } catch (RestApiException | InvalidQueryParameterException e) {
+            log.error("Failed to edit downstream topics of {}", change.id, e);
           }
-        } catch (RestApiException | InvalidQueryParameterException e) {
-          log.error("Failed to edit downstream topics of {}", change.id, e);
         }
       }
+    } catch (OrmException | ConfigInvalidException e) {
+      log.error("Automerger plugin failed onTopicEdited for {}", event.getChange().id, e);
     }
   }
 
@@ -169,43 +183,43 @@
    */
   @Override
   public void onCommentAdded(CommentAddedListener.Event event) {
-    RevisionInfo eventRevision = event.getRevision();
-    if (!eventRevision.isCurrent) {
-      log.info(
-          "Not updating downstream votes since revision {} is not current.", eventRevision._number);
-      return;
-    }
-    ChangeInfo change = event.getChange();
-    String revision = change.currentRevision;
-    Set<String> downstreamBranches;
-    try {
-      downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
-    } catch (RestApiException | IOException | ConfigInvalidException e) {
-      log.error("Failed to update downstream votes of {}", change.id, e);
-      return;
-    }
-
-    if (downstreamBranches.isEmpty()) {
-      log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
-      return;
-    }
-
-    Map<String, ApprovalInfo> approvals = event.getApprovals();
-
-    for (String downstreamBranch : downstreamBranches) {
-      try {
-        List<Integer> existingDownstream =
-            getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
-        for (Integer changeNumber : existingDownstream) {
-          ChangeInfo downstreamChange =
-              gApi.changes().id(changeNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
-          for (Map.Entry<String, ApprovalInfo> label : approvals.entrySet()) {
-            updateVote(downstreamChange, label.getKey(), label.getValue().value.shortValue());
-          }
-        }
-      } catch (RestApiException | InvalidQueryParameterException e) {
-        log.error("Exception when updating downstream votes of {}", change.id, e);
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      RevisionInfo eventRevision = event.getRevision();
+      if (!eventRevision.isCurrent) {
+        log.info(
+            "Not updating downstream votes since revision {} is not current.",
+            eventRevision._number);
+        return;
       }
+      ChangeInfo change = event.getChange();
+      String revision = change.currentRevision;
+      Set<String> downstreamBranches;
+      downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
+
+      if (downstreamBranches.isEmpty()) {
+        log.debug("Downstream branches of {} on {} are empty", change.branch, change.project);
+        return;
+      }
+
+      Map<String, ApprovalInfo> approvals = event.getApprovals();
+
+      for (String downstreamBranch : downstreamBranches) {
+        try {
+          List<Integer> existingDownstream =
+              getExistingMergesOnBranch(revision, change.topic, downstreamBranch);
+          for (Integer changeNumber : existingDownstream) {
+            ChangeInfo downstreamChange =
+                gApi.changes().id(changeNumber).get(EnumSet.of(ListChangesOption.CURRENT_REVISION));
+            for (Map.Entry<String, ApprovalInfo> label : approvals.entrySet()) {
+              updateVote(downstreamChange, label.getKey(), label.getValue().value.shortValue());
+            }
+          }
+        } catch (RestApiException | InvalidQueryParameterException e) {
+          log.error("Exception when updating downstream votes of {}", change.id, e);
+        }
+      }
+    } catch (OrmException | ConfigInvalidException | RestApiException | IOException e) {
+      log.error("Automerger plugin failed onCommentAdded for {}", event.getChange().id, e);
     }
   }
 
@@ -216,14 +230,15 @@
    */
   @Override
   public void onChangeRestored(ChangeRestoredListener.Event event) {
-    ChangeInfo change = event.getChange();
-    try {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ChangeInfo change = event.getChange();
       automergeChanges(change, event.getRevision());
     } catch (RestApiException
         | IOException
         | ConfigInvalidException
-        | InvalidQueryParameterException e) {
-      log.error("Automerger plugin failed onChangeRestored for {}", change.id, e);
+        | InvalidQueryParameterException
+        | OrmException e) {
+      log.error("Automerger plugin failed onChangeRestored for {}", event.getChange().id, e);
     }
   }
 
@@ -234,14 +249,15 @@
    */
   @Override
   public void onDraftPublished(DraftPublishedListener.Event event) {
-    ChangeInfo change = event.getChange();
-    try {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ChangeInfo change = event.getChange();
       automergeChanges(change, event.getRevision());
     } catch (RestApiException
         | IOException
         | ConfigInvalidException
-        | InvalidQueryParameterException e) {
-      log.error("Automerger plugin failed onDraftPublished for {}", change.id, e);
+        | InvalidQueryParameterException
+        | OrmException e) {
+      log.error("Automerger plugin failed onDraftPublished for {}", event.getChange().id, e);
     }
   }
 
@@ -252,14 +268,15 @@
    */
   @Override
   public void onRevisionCreated(RevisionCreatedListener.Event event) {
-    ChangeInfo change = event.getChange();
-    try {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ChangeInfo change = event.getChange();
       automergeChanges(change, event.getRevision());
     } catch (RestApiException
         | IOException
         | ConfigInvalidException
-        | InvalidQueryParameterException e) {
-      log.error("Automerger plugin failed onRevisionCreated for {}", change.id, e);
+        | InvalidQueryParameterException
+        | OrmException e) {
+      log.error("Automerger plugin failed onRevisionCreated for {}", event.getChange().id, e);
     }
   }
 
@@ -270,41 +287,45 @@
    * @throws RestApiException Throws if we fail a REST API call.
    * @throws ConfigInvalidException Throws if we get a malformed configuration
    * @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
+   * @throws OrmException Throws if we fail to open the request context
    */
   public void createMergesAndHandleConflicts(MultipleDownstreamMergeInput mdsMergeInput)
-      throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
-    ReviewInput reviewInput = new ReviewInput();
-    Map<String, Short> labels = new HashMap<String, Short>();
-    try {
-      createDownstreamMerges(mdsMergeInput);
+      throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
+          OrmException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      ReviewInput reviewInput = new ReviewInput();
+      Map<String, Short> labels = new HashMap<String, Short>();
+      try {
+        createDownstreamMerges(mdsMergeInput);
 
-      reviewInput.message =
-          "Automerging to "
-              + Joiner.on(", ").join(mdsMergeInput.dsBranchMap.keySet())
-              + " succeeded!";
-      reviewInput.notify = NotifyHandling.NONE;
-    } catch (FailedMergeException e) {
-      reviewInput.message = e.getDisplayString();
-      reviewInput.notify = NotifyHandling.ALL;
-      reviewInput.tag = MERGE_CONFLICT_TAG;
-      // Vote minAutomergeVote if we hit a conflict.
-      if (!config.minAutomergeVoteDisabled()) {
-        labels.put(config.getAutomergeLabel(), config.getMinAutomergeVote());
+        reviewInput.message =
+            "Automerging to "
+                + Joiner.on(", ").join(mdsMergeInput.dsBranchMap.keySet())
+                + " succeeded!";
+        reviewInput.notify = NotifyHandling.NONE;
+      } catch (FailedMergeException e) {
+        reviewInput.message = e.getDisplayString();
+        reviewInput.notify = NotifyHandling.ALL;
+        reviewInput.tag = MERGE_CONFLICT_TAG;
+        // Vote minAutomergeVote if we hit a conflict.
+        if (!config.minAutomergeVoteDisabled()) {
+          labels.put(config.getAutomergeLabel(), config.getMinAutomergeVote());
+        }
       }
-    }
-    reviewInput.labels = labels;
-    // if this fails, i.e. -2 is restricted, catch it and still post message without a vote.
-    try {
-      gApi.changes()
-          .id(mdsMergeInput.changeNumber)
-          .revision(mdsMergeInput.currentRevision)
-          .review(reviewInput);
-    } catch (AuthException e) {
-      reviewInput.labels = null;
-      gApi.changes()
-          .id(mdsMergeInput.changeNumber)
-          .revision(mdsMergeInput.currentRevision)
-          .review(reviewInput);
+      reviewInput.labels = labels;
+      // if this fails, i.e. -2 is restricted, catch it and still post message without a vote.
+      try {
+        gApi.changes()
+            .id(mdsMergeInput.changeNumber)
+            .revision(mdsMergeInput.currentRevision)
+            .review(reviewInput);
+      } catch (AuthException e) {
+        reviewInput.labels = null;
+        gApi.changes()
+            .id(mdsMergeInput.changeNumber)
+            .revision(mdsMergeInput.currentRevision)
+            .review(reviewInput);
+      }
     }
   }
 
@@ -316,76 +337,80 @@
    * @throws FailedMergeException Throws if we get a merge conflict when merging downstream.
    * @throws ConfigInvalidException Throws if we get a malformed config file
    * @throws InvalidQueryParameterException Throws if we attempt to add an invalid value to query.
+   * @throws OrmException Throws if we fail to open the request context
    */
   public void createDownstreamMerges(MultipleDownstreamMergeInput mdsMergeInput)
       throws RestApiException, FailedMergeException, ConfigInvalidException,
-          InvalidQueryParameterException {
-    // Map from branch to error message
-    Map<String, String> failedMergeBranchMap = new TreeMap<String, String>();
+          InvalidQueryParameterException, OrmException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      // Map from branch to error message
+      Map<String, String> failedMergeBranchMap = new TreeMap<String, String>();
 
-    List<Integer> existingDownstream;
-    for (String downstreamBranch : mdsMergeInput.dsBranchMap.keySet()) {
-      // If there are existing downstream merges, update them
-      // Otherwise, create them.
-      boolean createDownstreams = true;
-      if (mdsMergeInput.obsoleteRevision != null) {
-        existingDownstream =
-            getExistingMergesOnBranch(
-                mdsMergeInput.obsoleteRevision, mdsMergeInput.topic, downstreamBranch);
-        if (!existingDownstream.isEmpty()) {
-          log.debug(
-              "Attempting to update downstream merge of {} on branch {}",
-              mdsMergeInput.currentRevision,
-              downstreamBranch);
-          // existingDownstream should almost always be of length one, but
-          // it's possible to construct it so that it's not
-          for (Integer dsChangeNumber : existingDownstream) {
-            try {
-              updateDownstreamMerge(
-                  mdsMergeInput.currentRevision,
-                  mdsMergeInput.subject,
-                  dsChangeNumber,
-                  mdsMergeInput.dsBranchMap.get(downstreamBranch));
-              createDownstreams = false;
-            } catch (MergeConflictException e) {
-              failedMergeBranchMap.put(downstreamBranch, e.getMessage());
-              log.debug("Abandoning existing, obsolete {} due to merge conflict.", dsChangeNumber);
-              abandonChange(dsChangeNumber);
+      List<Integer> existingDownstream;
+      for (String downstreamBranch : mdsMergeInput.dsBranchMap.keySet()) {
+        // If there are existing downstream merges, update them
+        // Otherwise, create them.
+        boolean createDownstreams = true;
+        if (mdsMergeInput.obsoleteRevision != null) {
+          existingDownstream =
+              getExistingMergesOnBranch(
+                  mdsMergeInput.obsoleteRevision, mdsMergeInput.topic, downstreamBranch);
+          if (!existingDownstream.isEmpty()) {
+            log.debug(
+                "Attempting to update downstream merge of {} on branch {}",
+                mdsMergeInput.currentRevision,
+                downstreamBranch);
+            // existingDownstream should almost always be of length one, but
+            // it's possible to construct it so that it's not
+            for (Integer dsChangeNumber : existingDownstream) {
+              try {
+                updateDownstreamMerge(
+                    mdsMergeInput.currentRevision,
+                    mdsMergeInput.subject,
+                    dsChangeNumber,
+                    mdsMergeInput.dsBranchMap.get(downstreamBranch));
+                createDownstreams = false;
+              } catch (MergeConflictException e) {
+                failedMergeBranchMap.put(downstreamBranch, e.getMessage());
+                log.debug(
+                    "Abandoning existing, obsolete {} due to merge conflict.", dsChangeNumber);
+                abandonChange(dsChangeNumber);
+              }
             }
           }
         }
-      }
-      if (createDownstreams) {
-        log.debug(
-            "Attempting to create downstream merge of {} on branch {}",
-            mdsMergeInput.currentRevision,
-            downstreamBranch);
-        SingleDownstreamMergeInput sdsMergeInput = new SingleDownstreamMergeInput();
-        sdsMergeInput.currentRevision = mdsMergeInput.currentRevision;
-        sdsMergeInput.changeNumber = mdsMergeInput.changeNumber;
-        sdsMergeInput.project = mdsMergeInput.project;
-        sdsMergeInput.topic = mdsMergeInput.topic;
-        sdsMergeInput.subject = mdsMergeInput.subject;
-        sdsMergeInput.downstreamBranch = downstreamBranch;
-        sdsMergeInput.doMerge = mdsMergeInput.dsBranchMap.get(downstreamBranch);
-        try {
-          createSingleDownstreamMerge(sdsMergeInput);
-        } catch (MergeConflictException e) {
-          failedMergeBranchMap.put(downstreamBranch, e.getMessage());
+        if (createDownstreams) {
+          log.debug(
+              "Attempting to create downstream merge of {} on branch {}",
+              mdsMergeInput.currentRevision,
+              downstreamBranch);
+          SingleDownstreamMergeInput sdsMergeInput = new SingleDownstreamMergeInput();
+          sdsMergeInput.currentRevision = mdsMergeInput.currentRevision;
+          sdsMergeInput.changeNumber = mdsMergeInput.changeNumber;
+          sdsMergeInput.project = mdsMergeInput.project;
+          sdsMergeInput.topic = mdsMergeInput.topic;
+          sdsMergeInput.subject = mdsMergeInput.subject;
+          sdsMergeInput.downstreamBranch = downstreamBranch;
+          sdsMergeInput.doMerge = mdsMergeInput.dsBranchMap.get(downstreamBranch);
+          try {
+            createSingleDownstreamMerge(sdsMergeInput);
+          } catch (MergeConflictException e) {
+            failedMergeBranchMap.put(downstreamBranch, e.getMessage());
+          }
         }
       }
-    }
 
-    if (!failedMergeBranchMap.isEmpty()) {
-      throw new FailedMergeException(
-          failedMergeBranchMap,
-          mdsMergeInput.currentRevision,
-          config.getHostName(),
-          mdsMergeInput.project,
-          mdsMergeInput.changeNumber,
-          mdsMergeInput.patchsetNumber,
-          config.getConflictMessage(),
-          mdsMergeInput.topic);
+      if (!failedMergeBranchMap.isEmpty()) {
+        throw new FailedMergeException(
+            failedMergeBranchMap,
+            mdsMergeInput.currentRevision,
+            config.getHostName(),
+            mdsMergeInput.project,
+            mdsMergeInput.changeNumber,
+            mdsMergeInput.patchsetNumber,
+            config.getConflictMessage(),
+            mdsMergeInput.topic);
+      }
     }
   }
 
@@ -398,25 +423,30 @@
    * @return List of change numbers that are downstream of the given branch.
    * @throws RestApiException Throws when we fail a REST API call.
    * @throws InvalidQueryParameterException Throws when we try to add an invalid value to the query.
+   * @throws ConfigInvalidException Throws if we fail to read the config
+   * @throws OrmException Throws if we fail to open the request context
    */
   public List<Integer> getExistingMergesOnBranch(
       String upstreamRevision, String topic, String downstreamBranch)
-      throws RestApiException, InvalidQueryParameterException {
-    List<Integer> downstreamChangeNumbers = new ArrayList<Integer>();
-    List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch);
+      throws RestApiException, InvalidQueryParameterException, OrmException,
+          ConfigInvalidException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      List<Integer> downstreamChangeNumbers = new ArrayList<Integer>();
+      List<ChangeInfo> changes = getChangesInTopicAndBranch(topic, downstreamBranch);
 
-    for (ChangeInfo change : changes) {
-      String changeRevision = change.currentRevision;
-      RevisionInfo revision = change.revisions.get(changeRevision);
-      List<CommitInfo> parents = revision.commit.parents;
-      if (parents.size() > 1) {
-        String secondParent = parents.get(1).commit;
-        if (secondParent.equals(upstreamRevision)) {
-          downstreamChangeNumbers.add(change._number);
+      for (ChangeInfo change : changes) {
+        String changeRevision = change.currentRevision;
+        RevisionInfo revision = change.revisions.get(changeRevision);
+        List<CommitInfo> parents = revision.commit.parents;
+        if (parents.size() > 1) {
+          String secondParent = parents.get(1).commit;
+          if (secondParent.equals(upstreamRevision)) {
+            downstreamChangeNumbers.add(change._number);
+          }
         }
       }
+      return downstreamChangeNumbers;
     }
-    return downstreamChangeNumbers;
   }
 
   /**
@@ -426,63 +456,72 @@
    * @throws RestApiException
    * @throws ConfigInvalidException
    * @throws InvalidQueryParameterException
+   * @throws OrmException
    */
   public void createSingleDownstreamMerge(SingleDownstreamMergeInput sdsMergeInput)
-      throws RestApiException, ConfigInvalidException, InvalidQueryParameterException {
-    String currentTopic = getOrSetTopic(sdsMergeInput.changeNumber, sdsMergeInput.topic);
+      throws RestApiException, ConfigInvalidException, InvalidQueryParameterException,
+          OrmException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      String currentTopic = getOrSetTopic(sdsMergeInput.changeNumber, sdsMergeInput.topic);
 
-    if (isAlreadyMerged(sdsMergeInput, currentTopic)) {
-      log.info(
-          "Commit {} already merged into {}, not automerging again.",
-          sdsMergeInput.currentRevision,
-          sdsMergeInput.downstreamBranch);
-      return;
-    }
-
-    MergeInput mergeInput = new MergeInput();
-    mergeInput.source = sdsMergeInput.currentRevision;
-    mergeInput.strategy = "recursive";
-
-    log.debug("Creating downstream merge for {}", sdsMergeInput.currentRevision);
-    ChangeInput downstreamChangeInput = new ChangeInput();
-    downstreamChangeInput.project = sdsMergeInput.project;
-    downstreamChangeInput.branch = sdsMergeInput.downstreamBranch;
-    downstreamChangeInput.subject =
-        getSubjectForDownstreamMerge(sdsMergeInput.subject, sdsMergeInput.currentRevision, false);
-    downstreamChangeInput.topic = currentTopic;
-    downstreamChangeInput.merge = mergeInput;
-    downstreamChangeInput.notify = NotifyHandling.NONE;
-
-    downstreamChangeInput.baseChange =
-        getBaseChangeId(
-            getChangeParents(sdsMergeInput.changeNumber, sdsMergeInput.currentRevision),
+      if (isAlreadyMerged(sdsMergeInput, currentTopic)) {
+        log.info(
+            "Commit {} already merged into {}, not automerging again.",
+            sdsMergeInput.currentRevision,
             sdsMergeInput.downstreamBranch);
+        return;
+      }
 
-    if (!sdsMergeInput.doMerge) {
-      mergeInput.strategy = "ours";
+      MergeInput mergeInput = new MergeInput();
+      mergeInput.source = sdsMergeInput.currentRevision;
+      mergeInput.strategy = "recursive";
+
+      log.debug("Creating downstream merge for {}", sdsMergeInput.currentRevision);
+      ChangeInput downstreamChangeInput = new ChangeInput();
+      downstreamChangeInput.project = sdsMergeInput.project;
+      downstreamChangeInput.branch = sdsMergeInput.downstreamBranch;
       downstreamChangeInput.subject =
-          getSubjectForDownstreamMerge(sdsMergeInput.subject, sdsMergeInput.currentRevision, true);
-      log.debug(
-          "Skipping merge for {} to {}",
-          sdsMergeInput.currentRevision,
-          sdsMergeInput.downstreamBranch);
-    }
+          getSubjectForDownstreamMerge(sdsMergeInput.subject, sdsMergeInput.currentRevision, false);
+      downstreamChangeInput.topic = currentTopic;
+      downstreamChangeInput.merge = mergeInput;
+      downstreamChangeInput.notify = NotifyHandling.NONE;
 
-    ChangeApi downstreamChange = gApi.changes().create(downstreamChangeInput);
+      downstreamChangeInput.baseChange =
+          getBaseChangeId(
+              getChangeParents(sdsMergeInput.changeNumber, sdsMergeInput.currentRevision),
+              sdsMergeInput.downstreamBranch);
 
-    // Vote maxAutomergeVote on the change so we know it was successful.
-    if (!config.maxAutomergeVoteDisabled()) {
-      updateVote(downstreamChange.get(), config.getAutomergeLabel(), config.getMaxAutomergeVote());
+      if (!sdsMergeInput.doMerge) {
+        mergeInput.strategy = "ours";
+        downstreamChangeInput.subject =
+            getSubjectForDownstreamMerge(
+                sdsMergeInput.subject, sdsMergeInput.currentRevision, true);
+        log.debug(
+            "Skipping merge for {} to {}",
+            sdsMergeInput.currentRevision,
+            sdsMergeInput.downstreamBranch);
+      }
+
+      ChangeApi downstreamChange = gApi.changes().create(downstreamChangeInput);
+
+      // Vote maxAutomergeVote on the change so we know it was successful.
+      if (!config.maxAutomergeVoteDisabled()) {
+        updateVote(
+            downstreamChange.get(), config.getAutomergeLabel(), config.getMaxAutomergeVote());
+      }
     }
   }
 
-  public String getOrSetTopic(int sourceId, String topic) throws RestApiException {
-    if (isNullOrEmpty(topic)) {
-      topic = "am-" + UUID.randomUUID();
-      log.debug("Setting original change {} topic to {}", sourceId, topic);
-      gApi.changes().id(sourceId).topic(topic);
+  public String getOrSetTopic(int sourceId, String topic)
+      throws RestApiException, OrmException, ConfigInvalidException {
+    try (ManualRequestContext ctx = oneOffRequestContext.openAs(config.getContextUserId())) {
+      if (isNullOrEmpty(topic)) {
+        topic = "am-" + UUID.randomUUID();
+        log.debug("Setting original change {} topic to {}", sourceId, topic);
+        gApi.changes().id(sourceId).topic(topic);
+      }
+      return topic;
     }
-    return topic;
   }
 
   /**
@@ -522,7 +561,8 @@
   }
 
   private void automergeChanges(ChangeInfo change, RevisionInfo revisionInfo)
-      throws RestApiException, IOException, ConfigInvalidException, InvalidQueryParameterException {
+      throws RestApiException, IOException, ConfigInvalidException, InvalidQueryParameterException,
+          OrmException {
     if (revisionInfo.draft != null && revisionInfo.draft) {
       log.debug("Patchset {} is draft change, ignoring.", revisionInfo.commit.commit);
       return;
@@ -563,7 +603,8 @@
     createMergesAndHandleConflicts(mdsMergeInput);
   }
 
-  private void abandonDownstream(ChangeInfo change, String revision) throws ConfigInvalidException {
+  private void abandonDownstream(ChangeInfo change, String revision)
+      throws ConfigInvalidException, OrmException {
     try {
       Set<String> downstreamBranches = config.getDownstreamBranches(change.branch, change.project);
       if (downstreamBranches.isEmpty()) {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
index c0ef3f1..1a041b7 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/ConfigLoaderIT.java
@@ -22,11 +22,14 @@
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.PluginConfigFactory;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.HashSet;
@@ -46,6 +49,7 @@
   @Inject private AllProjectsName allProjectsName;
   @Inject private PluginConfigFactory cfgFactory;
   @Inject private String canonicalWebUrl;
+  @Inject private Provider<CurrentUser> user;
   private Project.NameKey manifestNameKey;
 
   @Test
@@ -229,6 +233,18 @@
     assertThat(configLoader.minAutomergeVoteDisabled()).isFalse();
   }
 
+  @Test
+  public void getContextUserIdTest() throws Exception {
+    defaultSetup("context_user.config");
+    assertThat(configLoader.getContextUserId()).isEqualTo(new Account.Id(102304));
+  }
+
+  @Test
+  public void getContextUserIdTest_noContextUser() throws Exception {
+    defaultSetup("automerger.config");
+    assertThat(configLoader.getContextUserId()).isEqualTo(user.get().getAccountId());
+  }
+
   private void setupTestRepo(
       String resourceName, Project.NameKey projectNameKey, String branchName, String filename)
       throws Exception {
@@ -264,6 +280,6 @@
   private void loadConfig(String configFilename) throws Exception {
     pushConfig(configFilename);
     configLoader =
-        new ConfigLoader(gApi, allProjectsName, "automerger", canonicalWebUrl, cfgFactory);
+        new ConfigLoader(gApi, allProjectsName, "automerger", canonicalWebUrl, cfgFactory, user);
   }
 }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
index eae3cd5..ae42611 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/automerger/DownstreamCreatorIT.java
@@ -22,18 +22,24 @@
 import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.extensions.api.accounts.AccountApi;
 import com.google.gerrit.extensions.api.changes.ChangeApi;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.group.SystemGroupBackend;
 import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.sql.Timestamp;
@@ -43,6 +49,7 @@
 import java.util.EnumSet;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
@@ -54,6 +61,8 @@
   sysModule = "com.googlesource.gerrit.plugins.automerger.AutomergerModule"
 )
 public class DownstreamCreatorIT extends LightweightPluginDaemonTest {
+  @Inject private Provider<CurrentUser> user;
+
   @Test
   public void testExpectedFlow() throws Exception {
     Project.NameKey manifestNameKey = defaultSetup();
@@ -548,7 +557,7 @@
     assertThat(getVote(masterChange, "Code-Review").value).isEqualTo(-2);
     assertThat(getVote(masterChange, "Code-Review").tag).isEqualTo("autogenerated:MergeConflict");
     assertThat(masterChangeInfo.branch).isEqualTo("master");
-    
+
     // Make sure that merge conflict message is still added
     List<String> messages = new ArrayList<>();
     for (ChangeMessageInfo cmi : masterChangeInfo.messages) {
@@ -587,10 +596,10 @@
     // Reset to allow our merge conflict to come
     testRepo.reset(initial);
     pushConfig("automerger.config", manifestNameKey.get(), projectName, "ds_one", "ds_two");
-    
+
     // Block Code Review label to test restrictions
     blockLabel("Code-Review", -2, 2, SystemGroupBackend.CHANGE_OWNER, "refs/heads/*", project);
-    
+
     // After we upload our config, we upload a new change to create the downstreams
     PushOneCommit.Result masterResult =
         pushFactory
@@ -619,7 +628,7 @@
     assertThat(getVote(masterChange, "Code-Review").value).isEqualTo(0);
     assertThat(getVote(masterChange, "Code-Review").tag).isEqualTo("autogenerated:MergeConflict");
     assertThat(masterChangeInfo.branch).isEqualTo("master");
-    
+
     // Make sure that merge conflict message is still added
     List<String> messages = new ArrayList<>();
     for (ChangeMessageInfo cmi : masterChangeInfo.messages) {
@@ -760,6 +769,72 @@
     merge(result);
   }
 
+  @Test
+  public void testContextUser() throws Exception {
+    Project.NameKey manifestNameKey = defaultSetup();
+    // Create initial change
+    PushOneCommit.Result initialResult = createChange("subject", "filename", "echo Hello");
+    // Project name is scoped by test, so we need to get it from our initial change
+    Project.NameKey projectNameKey = initialResult.getChange().project();
+    String projectName = projectNameKey.get();
+    createBranch(new Branch.NameKey(projectName, "ds_one"));
+    createBranch(new Branch.NameKey(projectName, "ds_two"));
+    initialResult.assertOkStatus();
+    merge(initialResult);
+
+    // Create normalUserGroup, containing current user, and contextUserGroup, containing contextUser
+    String normalUserGroup = createGroup("normalUserGroup");
+    gApi.groups().id(normalUserGroup).addMembers(user.get().getAccountId().toString());
+    AccountApi contextUserApi = gApi.accounts().create("someContextUser");
+    String contextUserGroup = createGroup("contextUserGroup");
+    gApi.groups().id(contextUserGroup).addMembers(contextUserApi.get().name);
+
+    // Grant exclusive +2 to context user
+    grantLabel(
+        "Code-Review",
+        -2,
+        2,
+        projectNameKey,
+        "refs/heads/ds_one",
+        false,
+        AccountGroup.UUID.parse(gApi.groups().id(contextUserGroup).get().id),
+        true);
+    pushContextUserConfig(manifestNameKey.get(), projectName, contextUserApi.get()._accountId);
+
+    // After we upload our config, we upload a new patchset to create the downstreams
+    PushOneCommit.Result result = createChange("subject", "filename2", "echo Hello", "sometopic");
+    result.assertOkStatus();
+    // Check that there are the correct number of changes in the topic
+    List<ChangeInfo> changesInTopic =
+        gApi.changes()
+            .query("topic: " + gApi.changes().id(result.getChangeId()).topic())
+            .withOptions(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT)
+            .get();
+    assertThat(changesInTopic).hasSize(3);
+
+    List<ChangeInfo> sortedChanges = sortedChanges(changesInTopic);
+
+    // Try to +2 downstream and see it fail
+    ChangeInfo dsOneChangeInfo = sortedChanges.get(0);
+    assertThat(dsOneChangeInfo.branch).isEqualTo("ds_one");
+    ChangeApi dsOneChange = gApi.changes().id(dsOneChangeInfo._number);
+    assertThat(getVote(dsOneChange, "Code-Review").value).isEqualTo(0);
+
+    exception.expect(AuthException.class);
+    exception.expectMessage("Applying label \"Code-Review\": 2 is restricted");
+    approve(dsOneChangeInfo.id);
+    assertThat(getVote(dsOneChange, "Code-Review").value).isEqualTo(0);
+
+    // Try to +2 master and see it succeed to +2 master and ds_one
+    ChangeInfo masterChangeInfo = sortedChanges.get(2);
+    assertThat(masterChangeInfo.branch).isEqualTo("master");
+    ChangeApi masterChange = gApi.changes().id(masterChangeInfo._number);
+    assertThat(getVote(masterChange, "Code-Review").value).isEqualTo(0);
+    approve(masterChangeInfo.id);
+    assertThat(getVote(masterChange, "Code-Review").value).isEqualTo(2);
+    assertThat(getVote(dsOneChange, "Code-Review").value).isEqualTo(2);
+  }
+
   private Project.NameKey defaultSetup() throws Exception {
     Project.NameKey manifestNameKey = createProject("platform/manifest");
     setupTestRepo("default.xml", manifestNameKey, "master", "default.xml");
@@ -830,6 +905,29 @@
     }
   }
 
+  private void pushContextUserConfig(String manifestName, String project, int contextUserId)
+      throws Exception {
+    TestRepository<InMemoryRepository> allProjectRepo = cloneProject(allProjects, admin);
+    GitUtil.fetch(allProjectRepo, RefNames.REFS_CONFIG + ":config");
+    allProjectRepo.reset("config");
+    try (InputStream in = getClass().getResourceAsStream("context_user.config")) {
+      String resourceString = CharStreams.toString(new InputStreamReader(in, Charsets.UTF_8));
+
+      Config cfg = new Config();
+      cfg.fromText(resourceString);
+      // Update manifest project path to the result of createProject(resourceName), since it is
+      // scoped to the test method
+      cfg.setString("global", null, "manifestProject", manifestName);
+      cfg.setInt("global", null, "contextUserId", contextUserId);
+      cfg.setString("automerger", "master:ds_one", "setProjects", project);
+      cfg.setString("automerger", "master:ds_two", "setProjects", project);
+      PushOneCommit push =
+          pushFactory.create(
+              db, admin.getIdent(), allProjectRepo, "Subject", "automerger.config", cfg.toText());
+      push.to(RefNames.REFS_CONFIG).assertOkStatus();
+    }
+  }
+
   private ApprovalInfo getVote(ChangeApi change, String label) throws RestApiException {
     return change.get(EnumSet.of(ListChangesOption.DETAILED_LABELS)).labels.get(label).all.get(0);
   }
diff --git a/src/test/resources/com/googlesource/gerrit/plugins/automerger/context_user.config b/src/test/resources/com/googlesource/gerrit/plugins/automerger/context_user.config
new file mode 100644
index 0000000..77f79b9
--- /dev/null
+++ b/src/test/resources/com/googlesource/gerrit/plugins/automerger/context_user.config
@@ -0,0 +1,19 @@
+[automerger "master:ds_one"]
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+[automerger "master:ds_two"]
+  mergeAll = true
+  addProjects = platform/added/project
+  ignoreProjects = whoo
+  setProjects = platform/some/project
+  setProjects = platform/other/project
+[automerger "ds_two:ds_three"]
+  setProjects = platform/some/project
+[global]
+  alwaysBlankMerge = .*Import translations. DO NOT MERGE.*
+  alwaysBlankMerge = .*DO NOT MERGE ANYWHERE.*
+  blankMerge = .*DO NOT MERGE.*
+  manifestFile = default.xml
+  manifestProject = platform/manifest
+  ignoreProjects = platform/ignore/me
+  contextUserId = 102304
\ No newline at end of file