Fix: Require contributor agreement for config changes
Apply the same contributor agreement check when creating a change that
updates project configuration.
For parity with the 'git push' path, this check should also happen when
updating project configs without creating a change, but that's more
likely to impact users, so it will be done in a follow-up change.
Bug: Issue 455720486
Release-Notes: Fixed project config update changes to require contributor agreement
Change-Id: I6e1604b4cce736a43d4aaf180aeac234cb2a9395
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
index a34b7d8..62c88276 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -42,6 +42,7 @@
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.ContributorAgreementsChecker;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.update.BatchUpdate;
@@ -75,6 +76,7 @@
private final PermissionBackend permissionBackend;
private final ChangeJson.Factory jsonFactory;
+ private final ContributorAgreementsChecker contributorAgreements;
@Inject
RepoMetaDataUpdater(
@@ -86,7 +88,8 @@
Sequences seq,
BatchUpdate.Factory updateFactory,
PermissionBackend permissionBackend,
- ChangeJson.Factory jsonFactory) {
+ ChangeJson.Factory jsonFactory,
+ ContributorAgreementsChecker contributorAgreements) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.user = user;
this.projectConfigFactory = projectConfigFactory;
@@ -96,6 +99,7 @@
this.updateFactory = updateFactory;
this.permissionBackend = permissionBackend;
this.jsonFactory = jsonFactory;
+ this.contributorAgreements = contributorAgreements;
}
/**
@@ -125,6 +129,8 @@
public ConfigChangeCreator configChangeCreator(
Project.NameKey projectName, @Nullable String message, String defaultMessage)
throws PermissionBackendException, AuthException, IOException, ConfigInvalidException {
+ contributorAgreements.check(projectName, user.get());
+
message = validateMessage(message, defaultMessage);
PermissionBackend.ForProject forProject =
permissionBackend.user(user.get()).project(projectName);
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 2803603..e7b548d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -23,8 +23,10 @@
import static java.util.Comparator.comparing;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
@@ -37,6 +39,11 @@
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRule;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.access.AccessSectionInfo;
+import com.google.gerrit.extensions.api.access.PermissionInfo;
+import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
+import com.google.gerrit.extensions.api.access.ProjectAccessInput;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -52,7 +59,10 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.testing.ConfigSuite;
+import com.google.gson.Strictness;
+import com.google.gson.stream.JsonReader;
import com.google.inject.Inject;
import java.util.List;
import org.eclipse.jgit.lib.Config;
@@ -362,6 +372,69 @@
}
@Test
+ public void createAccessChangeRespectsCLA() throws Exception {
+ assume().that(isContributorAgreementsEnabled()).isTrue();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
+ .add(allow(Permission.PUSH).ref("refs/for/" + RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ // Create an access change succeeds when agreement is not required
+ setUseContributorAgreements(InheritableBoolean.FALSE);
+ RestResponse resp = createPermissionsChange("agreement not required");
+ resp.assertCreated();
+ ChangeInfo got;
+ try (JsonReader jsonReader = new JsonReader(resp.getReader())) {
+ jsonReader.setStrictness(Strictness.LENIENT);
+ got = newGson().fromJson(jsonReader, ChangeInfo.class);
+ assertThat(got.subject).isEqualTo("agreement not required");
+ }
+
+ // Create an access change is not allowed when CLA is required but not signed
+ setUseContributorAgreements(InheritableBoolean.TRUE);
+ resp = createPermissionsChange("agreement required - fails");
+ resp.assertForbidden();
+ assertThat(resp.hasContent()).isTrue();
+ assertThat(resp.getEntityContent()).startsWith("No Contributor Agreement on file ");
+
+ // Sign the agreement
+ gApi.accounts().self().signAgreement(caAutoVerify.getName());
+
+ // Explicitly reset the user to force a new request context
+ requestScopeOperations.setApiUser(user.id());
+
+ // Create an access change succeeds after signing the agreement
+ resp = createPermissionsChange("agreement required - passes");
+ resp.assertCreated();
+ try (JsonReader jsonReader = new JsonReader(resp.getReader())) {
+ jsonReader.setStrictness(Strictness.LENIENT);
+ got = newGson().fromJson(jsonReader, ChangeInfo.class);
+ assertThat(got.subject).isEqualTo("agreement required - passes");
+ }
+ }
+
+ private RestResponse createPermissionsChange(String message) throws Exception {
+ ProjectAccessInput in = new ProjectAccessInput();
+ in.message = message;
+
+ PermissionInfo p = new PermissionInfo(null, null);
+ p.rules =
+ ImmutableMap.of(
+ SystemGroupBackend.REGISTERED_USERS.get(),
+ new PermissionRuleInfo(PermissionRuleInfo.Action.ALLOW, false));
+ AccessSectionInfo a = new AccessSectionInfo();
+ a.permissions = ImmutableMap.of("read", p);
+ in.add = ImmutableMap.of("refs/heads/*", a);
+
+ return userRestSession.put("/projects/" + project.get() + "/access:review", in);
+ }
+
+ @Test
public void createExcludedProjectChangeIgnoresCLA() throws Exception {
// Contributor agreements configured with excludeProjects = ExcludedProject
// in AbstractDaemonTest.configureContributorAgreement(...)