Factor out Contributor Agreements from ProjectControl

This commit factors out the check for valid CLAs from ProjectControl.
CLAs will continue to exist separate of permission backend as the fact
if a user has signed a CLA has nothing to do with permissions per-se.

This refactoring also removes the callers of
ProjectControl#canPushToAtLeastOneRef() which will be removed in a
follow-up change when the last caller was migrated.

Change-Id: Ib3f0849f9fbb720fee2cbc422127f7769a45a20f
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 0e6ae84..2adf029 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -62,6 +63,7 @@
       AllProjectsName allProjects,
       Provider<SetParent> setParent,
       GitReferenceUpdated gitRefUpdated,
+      ContributorAgreementsChecker contributorAgreements,
       @Assisted("projectName") Project.NameKey projectName,
       @Nullable @Assisted ObjectId base,
       @Assisted List<AccessSection> sectionList,
@@ -78,6 +80,7 @@
         sectionList,
         parentProjectName,
         message,
+        contributorAgreements,
         true);
     this.projectAccessFactory = projectAccessFactory;
     this.projectCache = projectCache;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index ecbcb39..3fa05ab 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -18,7 +18,6 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
@@ -37,6 +36,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefPattern;
@@ -58,6 +58,7 @@
   private final MetaDataUpdate.User metaDataUpdateFactory;
   private final AllProjectsName allProjects;
   private final Provider<SetParent> setParent;
+  private final ContributorAgreementsChecker contributorAgreements;
 
   protected final Project.NameKey projectName;
   protected final ObjectId base;
@@ -77,6 +78,7 @@
       List<AccessSection> sectionList,
       Project.NameKey parentProjectName,
       String message,
+      ContributorAgreementsChecker contributorAgreements,
       boolean checkIfOwner) {
     this.projectControlFactory = projectControlFactory;
     this.groupBackend = groupBackend;
@@ -89,6 +91,7 @@
     this.sectionList = sectionList;
     this.parentProjectName = parentProjectName;
     this.message = message;
+    this.contributorAgreements = contributorAgreements;
     this.checkIfOwner = checkIfOwner;
   }
 
@@ -99,9 +102,10 @@
           PermissionDeniedException, PermissionBackendException {
     final ProjectControl projectControl = projectControlFactory.controlFor(projectName);
 
-    Capable r = projectControl.canPushToAtLeastOneRef();
-    if (r != Capable.OK) {
-      throw new PermissionDeniedException(r.getMessage());
+    try {
+      contributorAgreements.check(projectName, projectControl.getUser());
+    } catch (AuthException e) {
+      throw new PermissionDeniedException(e.getMessage());
     }
 
     try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 4e2a4d3..f27b9d3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -43,6 +43,7 @@
 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.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.SetParent;
@@ -94,6 +95,7 @@
       BatchUpdate.Factory updateFactory,
       Provider<SetParent> setParent,
       Sequences seq,
+      ContributorAgreementsChecker contributorAgreements,
       @Assisted("projectName") Project.NameKey projectName,
       @Nullable @Assisted ObjectId base,
       @Assisted List<AccessSection> sectionList,
@@ -110,6 +112,7 @@
         sectionList,
         parentProjectName,
         message,
+        contributorAgreements,
         false);
     this.db = db;
     this.permissionBackend = permissionBackend;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index f980ade..7fffd3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -31,10 +31,10 @@
 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.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -54,7 +54,7 @@
   private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ContributorAgreementsChecker contributorAgreements;
 
   @Inject
   CherryPick(
@@ -63,13 +63,13 @@
       RetryHelper retryHelper,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
-      ProjectControl.GenericFactory projectControlFactory) {
+      ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
-    this.projectControlFactory = projectControlFactory;
+    this.contributorAgreements = contributorAgreements;
   }
 
   @Override
@@ -85,7 +85,8 @@
     }
 
     String refName = RefNames.fullName(input.destination);
-    CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
+    contributorAgreements.check(rsrc.getProject(), rsrc.getUser());
+
     permissionBackend
         .user(user)
         .project(rsrc.getChange().getProject())
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
index 0444e0a..4980975 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.CommitResource;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -51,6 +52,7 @@
   private final Provider<CurrentUser> user;
   private final CherryPickChange cherryPickChange;
   private final ChangeJson.Factory json;
+  private final ContributorAgreementsChecker contributorAgreements;
 
   @Inject
   CherryPickCommit(
@@ -58,12 +60,14 @@
       Provider<CurrentUser> user,
       CherryPickChange cherryPickChange,
       ChangeJson.Factory json,
-      PermissionBackend permissionBackend) {
+      PermissionBackend permissionBackend,
+      ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.permissionBackend = permissionBackend;
     this.user = user;
     this.cherryPickChange = cherryPickChange;
     this.json = json;
+    this.contributorAgreements = contributorAgreements;
   }
 
   @Override
@@ -83,7 +87,7 @@
     }
 
     String refName = RefNames.fullName(destination);
-    CreateChange.checkValidCLA(rsrc.getProjectState().controlFor(user.get()));
+    contributorAgreements.check(projectName, user.get());
     permissionBackend
         .user(user)
         .project(projectName)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index 3c5ae7d..ba8701c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -20,14 +20,12 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.MergeInput;
-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;
@@ -57,8 +55,8 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.CommitsCollection;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectResource;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.ProjectsCollection;
@@ -110,6 +108,7 @@
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
   private final NotifyUtil notifyUtil;
+  private final ContributorAgreementsChecker contributorAgreements;
 
   @Inject
   CreateChange(
@@ -130,7 +129,8 @@
       PatchSetUtil psUtil,
       @GerritServerConfig Config config,
       MergeUtil.Factory mergeUtilFactory,
-      NotifyUtil notifyUtil) {
+      NotifyUtil notifyUtil,
+      ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.anonymousCowardName = anonymousCowardName;
     this.db = db;
@@ -149,6 +149,7 @@
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.mergeUtilFactory = mergeUtilFactory;
     this.notifyUtil = notifyUtil;
+    this.contributorAgreements = contributorAgreements;
   }
 
   @Override
@@ -175,7 +176,7 @@
     }
 
     ProjectResource rsrc = projectsCollection.parse(input.project);
-    checkValidCLA(rsrc.getControl());
+    contributorAgreements.check(rsrc.getNameKey(), rsrc.getUser());
 
     Project.NameKey project = rsrc.getNameKey();
     String refName = RefNames.fullName(input.branch);
@@ -341,11 +342,4 @@
   private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
     return inserter.insert(new TreeFormatter());
   }
-
-  static void checkValidCLA(ProjectControl ctl) throws AuthException {
-    Capable capable = ctl.canPushToAtLeastOneRef();
-    if (capable != Capable.OK) {
-      throw new AuthException(capable.getMessage());
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index 0e57918..c4e2f3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -26,8 +26,8 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.edit.ChangeEdit;
 import com.google.gerrit.server.edit.ChangeEditUtil;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
 import com.google.gerrit.server.update.RetryingRestModifyView;
@@ -76,18 +76,18 @@
 
     private final ChangeEditUtil editUtil;
     private final NotifyUtil notifyUtil;
-    private final ProjectControl.GenericFactory projectControlFactory;
+    private final ContributorAgreementsChecker contributorAgreementsChecker;
 
     @Inject
     Publish(
         RetryHelper retryHelper,
         ChangeEditUtil editUtil,
         NotifyUtil notifyUtil,
-        ProjectControl.GenericFactory projectControlFactory) {
+        ContributorAgreementsChecker contributorAgreementsChecker) {
       super(retryHelper);
       this.editUtil = editUtil;
       this.notifyUtil = notifyUtil;
-      this.projectControlFactory = projectControlFactory;
+      this.contributorAgreementsChecker = contributorAgreementsChecker;
     }
 
     @Override
@@ -95,8 +95,7 @@
         BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in)
         throws IOException, OrmException, RestApiException, UpdateException, ConfigInvalidException,
             NoSuchProjectException {
-      CreateChange.checkValidCLA(
-          projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
+      contributorAgreementsChecker.check(rsrc.getProject(), rsrc.getUser());
       Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getNotes(), rsrc.getUser());
       if (!edit.isPresent()) {
         throw new ResourceConflictException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index cfb588f..2dfda08 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -46,9 +46,9 @@
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateOp;
 import com.google.gerrit.server.update.ChangeContext;
@@ -95,7 +95,7 @@
   private final PersonIdent serverIdent;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeReverted changeReverted;
-  private final ProjectControl.GenericFactory projectControlFactory;
+  private final ContributorAgreementsChecker contributorAgreements;
 
   @Inject
   Revert(
@@ -112,7 +112,7 @@
       @GerritPersonIdent PersonIdent serverIdent,
       ApprovalsUtil approvalsUtil,
       ChangeReverted changeReverted,
-      ProjectControl.GenericFactory projectControlFactory) {
+      ContributorAgreementsChecker contributorAgreements) {
     super(retryHelper);
     this.db = db;
     this.permissionBackend = permissionBackend;
@@ -126,7 +126,7 @@
     this.serverIdent = serverIdent;
     this.approvalsUtil = approvalsUtil;
     this.changeReverted = changeReverted;
-    this.projectControlFactory = projectControlFactory;
+    this.contributorAgreements = contributorAgreements;
   }
 
   @Override
@@ -139,7 +139,7 @@
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
     }
 
-    CreateChange.checkValidCLA(projectControlFactory.controlFor(rsrc.getProject(), rsrc.getUser()));
+    contributorAgreements.check(rsrc.getProject(), rsrc.getUser());
     permissionBackend.user(rsrc.getUser()).ref(change.getDest()).check(CREATE_CHANGE);
 
     Change.Id revertId =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 71d8f63..22834f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -32,6 +32,7 @@
 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.project.ContributorAgreementsChecker;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -45,6 +46,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.FactoryModuleBuilder;
 import com.google.inject.name.Named;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -167,6 +169,7 @@
   private final ExecutorService executor;
   private final RequestScopePropagator scopePropagator;
   private final ReceiveConfig receiveConfig;
+  private final ContributorAgreementsChecker contributorAgreements;
   private final long timeoutMillis;
   private final ProjectControl projectControl;
   private final Repository repo;
@@ -185,6 +188,7 @@
       ReceiveConfig receiveConfig,
       TransferConfig transferConfig,
       Provider<LazyPostReceiveHookChain> lazyPostReceive,
+      ContributorAgreementsChecker contributorAgreements,
       @Named(TIMEOUT_NAME) long timeoutMillis,
       @Assisted ProjectControl projectControl,
       @Assisted Repository repo,
@@ -195,6 +199,7 @@
     this.executor = executor;
     this.scopePropagator = scopePropagator;
     this.receiveConfig = receiveConfig;
+    this.contributorAgreements = contributorAgreements;
     this.timeoutMillis = timeoutMillis;
     this.projectControl = projectControl;
     this.repo = repo;
@@ -235,15 +240,23 @@
   }
 
   /** Determine if the user can upload commits. */
-  public Capable canUpload() {
+  public Capable canUpload() throws IOException {
     Capable result = projectControl.canPushToAtLeastOneRef();
     if (result != Capable.OK) {
       return result;
     }
-    if (receiveConfig.checkMagicRefs) {
-      result = MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject());
+
+    try {
+      contributorAgreements.check(
+          projectControl.getProject().getNameKey(), projectControl.getUser());
+    } catch (AuthException e) {
+      return new Capable(e.getMessage());
     }
-    return result;
+
+    if (receiveConfig.checkMagicRefs) {
+      return MagicBranch.checkMagicBranchRefs(repo, projectControl.getProject());
+    }
+    return Capable.OK;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
new file mode 100644
index 0000000..0033b12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ContributorAgreementsChecker.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2017 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.project;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.project.ProjectControl.Metrics;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+@Singleton
+public class ContributorAgreementsChecker {
+
+  private final String canonicalWebUrl;
+  private final ProjectCache projectCache;
+  private final Metrics metrics;
+
+  @Inject
+  ContributorAgreementsChecker(
+      @CanonicalWebUrl @Nullable String canonicalWebUrl,
+      ProjectCache projectCache,
+      Metrics metrics) {
+    this.canonicalWebUrl = canonicalWebUrl;
+    this.projectCache = projectCache;
+    this.metrics = metrics;
+  }
+
+  /**
+   * Checks if the user has signed a contributor agreement for the project.
+   *
+   * @throws AuthException if the user has not signed a contributor agreement for the project
+   * @throws IOException if project states could not be loaded
+   */
+  public void check(Project.NameKey project, CurrentUser user) throws IOException, AuthException {
+    metrics.claCheckCount.increment();
+
+    ProjectState projectState = projectCache.checkedGet(project);
+    if (projectState == null) {
+      throw new IOException("Can't load All-Projects");
+    }
+
+    if (!projectState.isUseContributorAgreements()) {
+      return;
+    }
+
+    if (!user.isIdentifiedUser()) {
+      throw new AuthException("Must be logged in to verify Contributor Agreement");
+    }
+
+    IdentifiedUser iUser = user.asIdentifiedUser();
+    Collection<ContributorAgreement> contributorAgreements =
+        projectCache.getAllProjects().getConfig().getContributorAgreements();
+    List<UUID> okGroupIds = new ArrayList<>();
+    for (ContributorAgreement ca : contributorAgreements) {
+      List<AccountGroup.UUID> groupIds;
+      groupIds = okGroupIds;
+
+      for (PermissionRule rule : ca.getAccepted()) {
+        if ((rule.getAction() == Action.ALLOW)
+            && (rule.getGroup() != null)
+            && (rule.getGroup().getUUID() != null)) {
+          groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+        }
+      }
+    }
+
+    if (!iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
+      final StringBuilder msg = new StringBuilder();
+      msg.append("A Contributor Agreement must be completed before uploading");
+      if (canonicalWebUrl != null) {
+        msg.append(":\n\n  ");
+        msg.append(canonicalWebUrl);
+        msg.append("#");
+        msg.append(PageLinks.SETTINGS_AGREEMENTS);
+        msg.append("\n");
+      } else {
+        msg.append(".");
+      }
+      throw new AuthException(msg.toString());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 8a0803d..db90199 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -19,14 +19,10 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.metrics.Counter0;
 import com.google.gerrit.metrics.Description;
@@ -38,9 +34,7 @@
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.gerrit.server.group.SystemGroupBackend;
@@ -60,7 +54,6 @@
 import com.google.inject.Singleton;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -131,15 +124,12 @@
 
   private final Set<AccountGroup.UUID> uploadGroups;
   private final Set<AccountGroup.UUID> receiveGroups;
-  private final String canonicalWebUrl;
   private final PermissionBackend.WithUser perm;
   private final CurrentUser user;
   private final ProjectState state;
   private final CommitsCollection commits;
   private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
-  private final Collection<ContributorAgreement> contributorAgreements;
-  private final Metrics metrics;
 
   private List<SectionMatcher> allSections;
   private Map<String, RefControl> refControls;
@@ -153,19 +143,14 @@
       PermissionCollection.Factory permissionFilter,
       CommitsCollection commits,
       ChangeControl.Factory changeControlFactory,
-      @CanonicalWebUrl @Nullable String canonicalWebUrl,
       PermissionBackend permissionBackend,
       @Assisted CurrentUser who,
-      @Assisted ProjectState ps,
-      Metrics metrics) {
+      @Assisted ProjectState ps) {
     this.changeControlFactory = changeControlFactory;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
     this.commits = commits;
-    this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
-    this.canonicalWebUrl = canonicalWebUrl;
-    this.metrics = metrics;
     this.perm = permissionBackend.user(who);
     user = who;
     state = ps;
@@ -221,16 +206,16 @@
     return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) || isAdmin();
   }
 
-  /** @return {@code Capable.OK} if the user can upload to at least one reference */
+  /**
+   * @return {@code Capable.OK} if the user can upload to at least one reference. Does not check
+   *     Contributor Agreements.
+   */
   public Capable canPushToAtLeastOneRef() {
     if (!canPerformOnAnyRef(Permission.PUSH)
         && !canPerformOnAnyRef(Permission.CREATE_TAG)
         && !isOwner()) {
       return new Capable("Upload denied for project '" + state.getName() + "'");
     }
-    if (state.isUseContributorAgreements()) {
-      return verifyActiveContributorAgreement();
-    }
     return Capable.OK;
   }
 
@@ -302,46 +287,6 @@
     return declaredOwner;
   }
 
-  private Capable verifyActiveContributorAgreement() {
-    metrics.claCheckCount.increment();
-    if (!(user.isIdentifiedUser())) {
-      return new Capable("Must be logged in to verify Contributor Agreement");
-    }
-    final IdentifiedUser iUser = user.asIdentifiedUser();
-
-    List<AccountGroup.UUID> okGroupIds = new ArrayList<>();
-    for (ContributorAgreement ca : contributorAgreements) {
-      List<AccountGroup.UUID> groupIds;
-      groupIds = okGroupIds;
-
-      for (PermissionRule rule : ca.getAccepted()) {
-        if ((rule.getAction() == Action.ALLOW)
-            && (rule.getGroup() != null)
-            && (rule.getGroup().getUUID() != null)) {
-          groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
-        }
-      }
-    }
-
-    if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
-      return Capable.OK;
-    }
-
-    final StringBuilder msg = new StringBuilder();
-    msg.append("A Contributor Agreement must be completed before uploading");
-    if (canonicalWebUrl != null) {
-      msg.append(":\n\n  ");
-      msg.append(canonicalWebUrl);
-      msg.append("#");
-      msg.append(PageLinks.SETTINGS_AGREEMENTS);
-      msg.append("\n");
-    } else {
-      msg.append(".");
-    }
-    msg.append("\n");
-    return new Capable(msg.toString());
-  }
-
   private boolean canPerformOnAnyRef(String permissionName) {
     for (SectionMatcher matcher : access()) {
       AccessSection section = matcher.section;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index b7a3788..4657652 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -207,7 +207,6 @@
   @Inject private SingleVersionListener singleVersionListener;
   @Inject private InMemoryDatabase schemaFactory;
   @Inject private ThreadLocalRequestContext requestContext;
-  @Inject private ProjectControl.Metrics metrics;
 
   @Before
   public void setUp() throws Exception {
@@ -875,11 +874,9 @@
         sectionSorter,
         null, // commitsCollection
         changeControlFactory,
-        "http://localhost", // canonicalWebUrl
         permissionBackend,
         new MockUser(name, memberOf),
-        newProjectState(local),
-        metrics);
+        newProjectState(local));
   }
 
   private ProjectState newProjectState(ProjectConfig local) {