Merge "Refactor SetAccess and CreateAccessChange."
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 338ff0d..3e8002b 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -14,17 +14,11 @@
 
 package com.google.gerrit.server.restapi.project;
 
-import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
-
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.Project;
-import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.exceptions.InvalidNameException;
 import com.google.gerrit.extensions.api.access.ProjectAccessInput;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -33,89 +27,35 @@
 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.server.Sequences;
-import com.google.gerrit.server.approval.ApprovalsUtil;
-import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeJson;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
-import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectResource;
-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.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
 public class CreateAccessChange implements RestModifyView<ProjectResource, ProjectAccessInput> {
-  private final PermissionBackend permissionBackend;
-  private final Sequences seq;
-  private final ChangeInserter.Factory changeInserterFactory;
-  private final BatchUpdate.Factory updateFactory;
-  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final SetAccessUtil setAccess;
   private final ChangeJson.Factory jsonFactory;
-  private final ProjectCache projectCache;
-  private final ProjectConfig.Factory projectConfigFactory;
+  private final RepoMetaDataUpdater repoMetaDataUpdater;
 
   @Inject
   CreateAccessChange(
-      PermissionBackend permissionBackend,
-      ChangeInserter.Factory changeInserterFactory,
-      BatchUpdate.Factory updateFactory,
-      Sequences seq,
-      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
       SetAccessUtil accessUtil,
       ChangeJson.Factory jsonFactory,
-      ProjectCache projectCache,
-      ProjectConfig.Factory projectConfigFactory) {
-    this.permissionBackend = permissionBackend;
-    this.seq = seq;
-    this.changeInserterFactory = changeInserterFactory;
-    this.updateFactory = updateFactory;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
+      RepoMetaDataUpdater repoMetaDataUpdater) {
     this.setAccess = accessUtil;
     this.jsonFactory = jsonFactory;
-    this.projectCache = projectCache;
-    this.projectConfigFactory = projectConfigFactory;
+    this.repoMetaDataUpdater = repoMetaDataUpdater;
   }
 
   @Override
   public Response<ChangeInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
-      throws PermissionBackendException, AuthException, IOException, ConfigInvalidException,
-          InvalidNameException, UpdateException, RestApiException {
-    PermissionBackend.ForProject forProject =
-        permissionBackend.user(rsrc.getUser()).project(rsrc.getNameKey());
-    if (!check(forProject, ProjectPermission.READ_CONFIG)) {
-      throw new AuthException(RefNames.REFS_CONFIG + " not visible");
-    }
-    if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
-      try {
-        forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
-      } catch (AuthException denied) {
-        throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG, denied);
-      }
-    }
-    projectCache
-        .get(rsrc.getNameKey())
-        .orElseThrow(illegalState(rsrc.getNameKey()))
-        .checkStatePermitsWrite();
-
-    MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
+      throws PermissionBackendException, IOException, ConfigInvalidException, InvalidNameException,
+          UpdateException, RestApiException {
     ImmutableList<AccessSection> removals =
         setAccess.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
     ImmutableList<AccessSection> additions =
@@ -123,81 +63,30 @@
 
     Project.NameKey newParentProjectName =
         input.parent == null ? null : Project.nameKey(input.parent);
-
-    try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
-      ProjectConfig config = projectConfigFactory.read(md);
-      ObjectId oldCommit = config.getRevision();
-      String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
-
-      setAccess.validateChanges(config, removals, additions);
-      setAccess.applyChanges(config, removals, additions);
-      try {
-        setAccess.setParentName(
-            rsrc.getUser().asIdentifiedUser(),
-            config,
-            rsrc.getNameKey(),
-            newParentProjectName,
-            false);
-      } catch (AuthException e) {
-        throw new IllegalStateException(e);
-      }
-
-      if (!Strings.isNullOrEmpty(input.message)) {
-        if (!input.message.endsWith("\n")) {
-          input.message += "\n";
-        }
-        md.setMessage(input.message);
-      } else {
-        md.setMessage("Review access change\n");
-      }
-
-      md.setInsertChangeId(true);
-      Change.Id changeId = Change.id(seq.nextChangeId());
-      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
-        RevCommit commit =
-            config.commitToNewRef(
-                md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
-
-        if (commit.name().equals(oldCommitSha1)) {
-          throw new BadRequestException("no change");
-        }
-
-        try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
-            ObjectReader objReader = objInserter.newReader();
-            RevWalk rw = new RevWalk(objReader);
-            BatchUpdate bu =
-                updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
-          bu.setRepository(md.getRepository(), rw, objInserter);
-          ChangeInserter ins = newInserter(changeId, commit);
-          bu.insertChange(ins);
-          bu.execute();
-          return Response.created(jsonFactory.noOptions().format(ins.getChange()));
-        }
-      }
+    String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Review access change";
+    try {
+      Change change =
+          repoMetaDataUpdater.updateAndCreateChangeForReview(
+              rsrc.getNameKey(),
+              rsrc.getUser(),
+              message,
+              config -> {
+                setAccess.validateChanges(config, removals, additions);
+                setAccess.applyChanges(config, removals, additions);
+                try {
+                  setAccess.setParentName(
+                      rsrc.getUser().asIdentifiedUser(),
+                      config,
+                      rsrc.getNameKey(),
+                      newParentProjectName,
+                      false);
+                } catch (AuthException e) {
+                  throw new IllegalStateException(e);
+                }
+              });
+      return Response.created(jsonFactory.noOptions().format(change));
     } catch (InvalidNameException e) {
       throw new BadRequestException(e.toString());
     }
   }
-
-  // ProjectConfig doesn't currently support fusing into a BatchUpdate.
-  @SuppressWarnings("deprecation")
-  private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
-    return changeInserterFactory
-        .create(changeId, commit, RefNames.REFS_CONFIG)
-        .setMessage(
-            // Same message as in ReceiveCommits.CreateRequest.
-            ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
-        .setValidate(false)
-        .setUpdateRef(false);
-  }
-
-  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
-      throws PermissionBackendException {
-    try {
-      perm.check(p);
-      return true;
-    } catch (AuthException denied) {
-      return false;
-    }
-  }
 }
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
new file mode 100644
index 0000000..c45a009
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -0,0 +1,215 @@
+// Copyright (C) 2024 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.google.gerrit.server.restapi.project;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.InvalidNameException;
+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.server.CreateGroupPermissionSyncer;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.approval.ApprovalsUtil;
+import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.git.meta.MetaDataUpdate.User;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+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 java.io.IOException;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Updates repo refs/meta/config content. */
+@Singleton
+public class RepoMetaDataUpdater {
+  private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
+  private final Provider<User> metaDataUpdateFactory;
+  private final ProjectConfig.Factory projectConfigFactory;
+  private final ProjectCache projectCache;
+  private final ChangeInserter.Factory changeInserterFactory;
+  private final Sequences seq;
+
+  private final BatchUpdate.Factory updateFactory;
+
+  private final PermissionBackend permissionBackend;
+
+  @Inject
+  RepoMetaDataUpdater(
+      CreateGroupPermissionSyncer createGroupPermissionSyncer,
+      Provider<User> metaDataUpdateFactory,
+      ProjectConfig.Factory projectConfigFactory,
+      ProjectCache projectCache,
+      ChangeInserter.Factory changeInserterFactory,
+      Sequences seq,
+      BatchUpdate.Factory updateFactory,
+      PermissionBackend permissionBackend) {
+    this.createGroupPermissionSyncer = createGroupPermissionSyncer;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.projectConfigFactory = projectConfigFactory;
+    this.projectCache = projectCache;
+    this.changeInserterFactory = changeInserterFactory;
+    this.seq = seq;
+    this.updateFactory = updateFactory;
+    this.permissionBackend = permissionBackend;
+  }
+
+  public Change updateAndCreateChangeForReview(
+      Project.NameKey projectName,
+      CurrentUser user,
+      String message,
+      ProjectConfigUpdater projectConfigUpdater)
+      throws ConfigInvalidException, IOException, RestApiException, UpdateException,
+          InvalidNameException, PermissionBackendException {
+    checkArgument(!message.isBlank(), "The message must not be empty");
+    message = validateMessage(message);
+
+    PermissionBackend.ForProject forProject = permissionBackend.user(user).project(projectName);
+    if (!check(forProject, ProjectPermission.READ_CONFIG)) {
+      throw new AuthException(RefNames.REFS_CONFIG + " not visible");
+    }
+    if (!check(forProject, ProjectPermission.WRITE_CONFIG)) {
+      try {
+        forProject.ref(RefNames.REFS_CONFIG).check(RefPermission.CREATE_CHANGE);
+      } catch (AuthException denied) {
+        throw new AuthException("cannot create change for " + RefNames.REFS_CONFIG, denied);
+      }
+    }
+    projectCache.get(projectName).orElseThrow(illegalState(projectName)).checkStatePermitsWrite();
+
+    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
+      ProjectConfig config = projectConfigFactory.read(md);
+      ObjectId oldCommit = config.getRevision();
+      String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
+
+      projectConfigUpdater.update(config);
+      md.setMessage(message);
+      md.setInsertChangeId(true);
+
+      Change.Id changeId = Change.id(seq.nextChangeId());
+      try (RefUpdateContext ctx = RefUpdateContext.open(CHANGE_MODIFICATION)) {
+        RevCommit commit =
+            config.commitToNewRef(
+                md, PatchSet.id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
+
+        if (commit.name().equals(oldCommitSha1)) {
+          throw new BadRequestException("no change");
+        }
+
+        try (ObjectInserter objInserter = md.getRepository().newObjectInserter();
+            ObjectReader objReader = objInserter.newReader();
+            RevWalk rw = new RevWalk(objReader);
+            BatchUpdate bu = updateFactory.create(projectName, user, TimeUtil.now())) {
+          bu.setRepository(md.getRepository(), rw, objInserter);
+          ChangeInserter ins = newInserter(changeId, commit);
+          bu.insertChange(ins);
+          bu.execute();
+          return ins.getChange();
+        }
+      }
+    }
+  }
+
+  public void updateWithoutReview(
+      Project.NameKey projectName, String message, ProjectConfigUpdater projectConfigUpdater)
+      throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
+          ResourceConflictException, InvalidNameException, BadRequestException {
+    updateWithoutReview(
+        projectName, message, /*skipPermissionsCheck=*/ false, projectConfigUpdater);
+  }
+
+  public void updateWithoutReview(
+      Project.NameKey projectName,
+      String message,
+      boolean skipPermissionsCheck,
+      ProjectConfigUpdater projectConfigUpdater)
+      throws ConfigInvalidException, IOException, PermissionBackendException, AuthException,
+          ResourceConflictException, InvalidNameException, BadRequestException {
+    message = validateMessage(message);
+    if (!skipPermissionsCheck) {
+      permissionBackend.currentUser().project(projectName).check(ProjectPermission.WRITE_CONFIG);
+    }
+
+    try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
+      ProjectConfig config = projectConfigFactory.read(md);
+
+      projectConfigUpdater.update(config);
+      md.setMessage(message);
+      config.commit(md);
+      projectCache.evictAndReindex(config.getProject());
+      createGroupPermissionSyncer.syncIfNeeded();
+    }
+  }
+
+  private String validateMessage(String message) {
+    if (!message.endsWith("\n")) {
+      return message + "\n";
+    }
+    return message;
+  }
+
+  // ProjectConfig doesn't currently support fusing into a BatchUpdate.
+  @SuppressWarnings("deprecation")
+  private ChangeInserter newInserter(Change.Id changeId, RevCommit commit) {
+    return changeInserterFactory
+        .create(changeId, commit, RefNames.REFS_CONFIG)
+        .setMessage(
+            // Same message as in ReceiveCommits.CreateRequest.
+            ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
+        .setValidate(false)
+        .setUpdateRef(false);
+  }
+
+  private boolean check(PermissionBackend.ForProject perm, ProjectPermission p)
+      throws PermissionBackendException {
+    try {
+      perm.check(p);
+      return true;
+    } catch (AuthException denied) {
+      return false;
+    }
+  }
+
+  @FunctionalInterface
+  public interface ProjectConfigUpdater {
+    void update(ProjectConfig config)
+        throws BadRequestException, InvalidNameException, PermissionBackendException,
+            ResourceConflictException, AuthException;
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccess.java b/java/com/google/gerrit/server/restapi/project/SetAccess.java
index e4e4373..75fe280 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccess.java
@@ -29,15 +29,11 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.server.CreateGroupPermissionSyncer;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -49,92 +45,72 @@
 public class SetAccess implements RestModifyView<ProjectResource, ProjectAccessInput> {
   protected final GroupBackend groupBackend;
   private final PermissionBackend permissionBackend;
-  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
   private final GetAccess getAccess;
-  private final ProjectCache projectCache;
   private final Provider<IdentifiedUser> identifiedUser;
   private final SetAccessUtil accessUtil;
-  private final CreateGroupPermissionSyncer createGroupPermissionSyncer;
-  private final ProjectConfig.Factory projectConfigFactory;
+  private final RepoMetaDataUpdater repoMetaDataUpdater;
 
   @Inject
   private SetAccess(
       GroupBackend groupBackend,
       PermissionBackend permissionBackend,
-      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
-      ProjectCache projectCache,
       GetAccess getAccess,
       Provider<IdentifiedUser> identifiedUser,
       SetAccessUtil accessUtil,
-      CreateGroupPermissionSyncer createGroupPermissionSyncer,
-      ProjectConfig.Factory projectConfigFactory) {
+      RepoMetaDataUpdater repoMetaDataUpdater) {
     this.groupBackend = groupBackend;
     this.permissionBackend = permissionBackend;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
     this.getAccess = getAccess;
-    this.projectCache = projectCache;
     this.identifiedUser = identifiedUser;
     this.accessUtil = accessUtil;
-    this.createGroupPermissionSyncer = createGroupPermissionSyncer;
-    this.projectConfigFactory = projectConfigFactory;
+    this.repoMetaDataUpdater = repoMetaDataUpdater;
   }
 
   @Override
   public Response<ProjectAccessInfo> apply(ProjectResource rsrc, ProjectAccessInput input)
       throws Exception {
-    MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get();
-
     validateInput(input);
 
-    ProjectConfig config;
-
     ImmutableList<AccessSection> removals =
         accessUtil.getAccessSections(input.remove, /* rejectNonResolvableGroups= */ false);
     ImmutableList<AccessSection> additions =
         accessUtil.getAccessSections(input.add, /* rejectNonResolvableGroups= */ true);
-    try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
-      config = projectConfigFactory.read(md);
-
-      // Check that the user has the right permissions.
-      boolean checkedAdmin = false;
-      for (AccessSection section : Iterables.concat(additions, removals)) {
-        boolean isGlobalCapabilities = AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
-        if (isGlobalCapabilities) {
-          if (!checkedAdmin) {
-            permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
-            checkedAdmin = true;
-          }
-        } else {
-          permissionBackend
-              .currentUser()
-              .project(rsrc.getNameKey())
-              .ref(section.getName())
-              .check(RefPermission.WRITE_CONFIG);
-        }
-      }
-
-      accessUtil.validateChanges(config, removals, additions);
-      accessUtil.applyChanges(config, removals, additions);
-
-      accessUtil.setParentName(
-          identifiedUser.get(),
-          config,
+    String message = !Strings.isNullOrEmpty(input.message) ? input.message : "Modify access rules";
+    try {
+      this.repoMetaDataUpdater.updateWithoutReview(
           rsrc.getNameKey(),
-          input.parent == null ? null : Project.nameKey(input.parent),
-          !checkedAdmin);
+          message,
+          /*skipPermissionsCheck=*/ true,
+          config -> {
+            // Check that the user has the right permissions.
+            boolean checkedAdmin = false;
+            for (AccessSection section : Iterables.concat(additions, removals)) {
+              boolean isGlobalCapabilities =
+                  AccessSection.GLOBAL_CAPABILITIES.equals(section.getName());
+              if (isGlobalCapabilities) {
+                if (!checkedAdmin) {
+                  permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+                  checkedAdmin = true;
+                }
+              } else {
+                permissionBackend
+                    .currentUser()
+                    .project(rsrc.getNameKey())
+                    .ref(section.getName())
+                    .check(RefPermission.WRITE_CONFIG);
+              }
+            }
 
-      if (!Strings.isNullOrEmpty(input.message)) {
-        if (!input.message.endsWith("\n")) {
-          input.message += "\n";
-        }
-        md.setMessage(input.message);
-      } else {
-        md.setMessage("Modify access rules\n");
-      }
+            accessUtil.validateChanges(config, removals, additions);
+            accessUtil.applyChanges(config, removals, additions);
 
-      config.commit(md);
-      projectCache.evictAndReindex(config.getProject());
-      createGroupPermissionSyncer.syncIfNeeded();
+            accessUtil.setParentName(
+                identifiedUser.get(),
+                config,
+                rsrc.getNameKey(),
+                input.parent == null ? null : Project.nameKey(input.parent),
+                !checkedAdmin);
+          });
     } catch (InvalidNameException e) {
       throw new BadRequestException(e.toString());
     } catch (ConfigInvalidException e) {