Hide ChangeControl#isVisible and migrate callers to PermissionBackend

Change-Id: I8cec3c3c182e0ca2b684370e5beeae11a95b69a1
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
index 308c9a5..bb7da11 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.change.Submit;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
@@ -304,7 +305,8 @@
   }
 
   private void assertChangeSetMergeable(ChangeData change, boolean expected)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException {
+      throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException,
+          PermissionBackendException {
     ChangeSet cs = mergeSuperSet.get().completeChangeSet(db, change.change(), user(admin));
     assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
index 3cc7335..8bfd880 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefEvent;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
@@ -86,7 +87,8 @@
   }
 
   @Override
-  public void postEvent(Change change, ChangeEvent event) throws OrmException {
+  public void postEvent(Change change, ChangeEvent event)
+      throws OrmException, PermissionBackendException {
     fireEvent(change, event);
   }
 
@@ -101,7 +103,7 @@
   }
 
   @Override
-  public void postEvent(Event event) throws OrmException {
+  public void postEvent(Event event) throws OrmException, PermissionBackendException {
     fireEvent(event);
   }
 
@@ -111,7 +113,8 @@
     }
   }
 
-  protected void fireEvent(Change change, ChangeEvent event) throws OrmException {
+  protected void fireEvent(Change change, ChangeEvent event)
+      throws OrmException, PermissionBackendException {
     for (UserScopedEventListener listener : listeners) {
       if (isVisibleTo(change, listener.getUser())) {
         listener.onEvent(event);
@@ -138,7 +141,7 @@
     fireEventForUnrestrictedListeners(event);
   }
 
-  protected void fireEvent(Event event) throws OrmException {
+  protected void fireEvent(Event event) throws OrmException, PermissionBackendException {
     for (UserScopedEventListener listener : listeners) {
       if (isVisibleTo(event, listener.getUser())) {
         listener.onEvent(event);
@@ -156,7 +159,8 @@
     }
   }
 
-  protected boolean isVisibleTo(Change change, CurrentUser user) throws OrmException {
+  protected boolean isVisibleTo(Change change, CurrentUser user)
+      throws OrmException, PermissionBackendException {
     if (change == null) {
       return false;
     }
@@ -164,9 +168,12 @@
     if (pe == null) {
       return false;
     }
-    ProjectControl pc = pe.controlFor(user);
     ReviewDb db = dbProvider.get();
-    return pc.controlFor(db, change).isVisible(db);
+    return permissionBackend
+        .user(user)
+        .change(notesFactory.createChecked(db, change))
+        .database(db)
+        .test(ChangePermission.READ);
   }
 
   protected boolean isVisibleTo(Branch.NameKey branchName, CurrentUser user) {
@@ -178,7 +185,8 @@
     return pc.controlForRef(branchName).isVisible();
   }
 
-  protected boolean isVisibleTo(Event event, CurrentUser user) throws OrmException {
+  protected boolean isVisibleTo(Event event, CurrentUser user)
+      throws OrmException, PermissionBackendException {
     if (event instanceof RefEvent) {
       RefEvent refEvent = (RefEvent) event;
       String ref = refEvent.getRefName();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
index 20d55d6..947b656 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventDispatcher.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.events.Event;
 import com.google.gerrit.server.events.ProjectEvent;
 import com.google.gerrit.server.events.RefEvent;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 
 /** Interface for posting (dispatching) Events */
@@ -31,8 +32,9 @@
    * @param change The change that the event is related to
    * @param event The event to post
    * @throws OrmException on failure to post the event due to DB error
+   * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Change change, ChangeEvent event) throws OrmException;
+  void postEvent(Change change, ChangeEvent event) throws OrmException, PermissionBackendException;
 
   /**
    * Post a stream event that is related to a branch
@@ -58,6 +60,7 @@
    *
    * @param event The event to post.
    * @throws OrmException on failure to post the event due to DB error
+   * @throws PermissionBackendException on failure of permission checks
    */
-  void postEvent(Event event) throws OrmException;
+  void postEvent(Event event) throws OrmException, PermissionBackendException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index e4cca59..3b8723b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -47,6 +47,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.util.LabelVote;
 import com.google.gwtorm.server.OrmException;
@@ -108,6 +111,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ApprovalCopier copier;
+  private final PermissionBackend permissionBackend;
 
   @VisibleForTesting
   @Inject
@@ -115,11 +119,13 @@
       NotesMigration migration,
       IdentifiedUser.GenericFactory userFactory,
       ChangeControl.GenericFactory changeControlFactory,
-      ApprovalCopier copier) {
+      ApprovalCopier copier,
+      PermissionBackend permissionBackend) {
     this.migration = migration;
     this.userFactory = userFactory;
     this.changeControlFactory = changeControlFactory;
     this.copier = copier;
+    this.permissionBackend = permissionBackend;
   }
 
   /**
@@ -262,8 +268,8 @@
   private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) {
     try {
       IdentifiedUser user = userFactory.create(accountId);
-      return changeControlFactory.controlFor(notes, user).isVisible(db);
-    } catch (OrmException e) {
+      return permissionBackend.user(user).change(notes).database(db).test(ChangePermission.READ);
+    } catch (PermissionBackendException e) {
       log.warn(
           String.format(
               "Failed to check if account %d can see change %d",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
index 995aaa5..a6c844b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/StarredChanges.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -67,7 +68,7 @@
 
   @Override
   public AccountResource.StarredChange parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     if (starredChangesUtil
@@ -104,7 +105,7 @@
       return createProvider.get().setChange(changes.parse(TopLevelResource.INSTANCE, id));
     } catch (ResourceNotFoundException e) {
       throw new UnprocessableEntityException(String.format("change %s not found", id.get()));
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("cannot resolve change", e);
       throw new UnprocessableEntityException("internal server error");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
index 52c6cdf..860f396 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Stars.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.account.AccountResource.Star;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -65,7 +66,7 @@
 
   @Override
   public Star parse(AccountResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     IdentifiedUser user = parent.getUser();
     ChangeResource change = changes.parse(TopLevelResource.INSTANCE, id);
     Set<String> labels = starredChangesUtil.getLabels(user.getAccountId(), change.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 3e8a146..d400999 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -53,7 +53,9 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -475,8 +477,12 @@
             accountId -> {
               try {
                 IdentifiedUser user = userFactory.create(accountId);
-                return changeControlFactory.controlFor(notes, user).isVisible(db);
-              } catch (OrmException e) {
+                return permissionBackend
+                    .user(user)
+                    .change(notes)
+                    .database(db)
+                    .test(ChangePermission.READ);
+              } catch (PermissionBackendException e) {
                 log.warn(
                     String.format(
                         "Failed to check if account %d can see change %d",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
index eeb1ab3..d967ab8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -26,6 +26,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.QueryChanges;
 import com.google.gwtorm.server.OrmException;
@@ -44,6 +47,7 @@
   private final ChangeFinder changeFinder;
   private final CreateChange createChange;
   private final ChangeResource.Factory changeResourceFactory;
+  private final PermissionBackend permissionBackend;
 
   @Inject
   ChangesCollection(
@@ -53,7 +57,8 @@
       DynamicMap<RestView<ChangeResource>> views,
       ChangeFinder changeFinder,
       CreateChange createChange,
-      ChangeResource.Factory changeResourceFactory) {
+      ChangeResource.Factory changeResourceFactory,
+      PermissionBackend permissionBackend) {
     this.db = db;
     this.user = user;
     this.queryFactory = queryFactory;
@@ -61,6 +66,7 @@
     this.changeFinder = changeFinder;
     this.createChange = createChange;
     this.changeResourceFactory = changeResourceFactory;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -75,7 +81,7 @@
 
   @Override
   public ChangeResource parse(TopLevelResource root, IdString id)
-      throws ResourceNotFoundException, OrmException {
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     List<ChangeControl> ctls = changeFinder.find(id.encoded(), user.get());
     if (ctls.isEmpty()) {
       throw new ResourceNotFoundException(id);
@@ -84,13 +90,14 @@
     }
 
     ChangeControl ctl = ctls.get(0);
-    if (!ctl.isVisible(db.get())) {
+    if (!canRead(ctl)) {
       throw new ResourceNotFoundException(id);
     }
     return changeResourceFactory.create(ctl);
   }
 
-  public ChangeResource parse(Change.Id id) throws ResourceNotFoundException, OrmException {
+  public ChangeResource parse(Change.Id id)
+      throws ResourceNotFoundException, OrmException, PermissionBackendException {
     List<ChangeControl> ctls = changeFinder.find(id, user.get());
     if (ctls.isEmpty()) {
       throw new ResourceNotFoundException(toIdString(id));
@@ -99,7 +106,7 @@
     }
 
     ChangeControl ctl = ctls.get(0);
-    if (!ctl.isVisible(db.get())) {
+    if (!canRead(ctl)) {
       throw new ResourceNotFoundException(toIdString(id));
     }
     return changeResourceFactory.create(ctl);
@@ -118,4 +125,12 @@
   public CreateChange post(TopLevelResource parent) throws RestApiException {
     return createChange;
   }
+
+  private boolean canRead(ChangeControl ctl) throws PermissionBackendException {
+    return permissionBackend
+        .user(user)
+        .change(ctl.getNotes())
+        .database(db)
+        .test(ChangePermission.READ);
+  }
 }
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 92ad46d..39eb8d4 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
@@ -52,6 +52,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -195,7 +196,11 @@
           throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         ChangeControl ctl = Iterables.getOnlyElement(ctls);
-        if (!ctl.isVisible(db.get())) {
+        if (!permissionBackend
+            .user(user)
+            .change(ctl.getNotes())
+            .database(db)
+            .test(ChangePermission.READ)) {
           throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
index 8a6a1ab..cb77fd1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
@@ -74,7 +75,7 @@
         changeResourceFactory.create(cd.changeControl()).prepareETag(h, user);
       }
       h.putBoolean(cs.furtherHiddenChanges());
-    } catch (IOException | OrmException e) {
+    } catch (IOException | OrmException | PermissionBackendException e) {
       throw new OrmRuntimeException(e);
     }
     return h.hash().toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index a7711b6..fb40b41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -56,6 +56,7 @@
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.RefPermission;
@@ -338,8 +339,12 @@
       ReviewerState state,
       NotifyHandling notify,
       ListMultimap<RecipientType, Account.Id> accountsToNotify)
-      throws OrmException {
-    if (!rsrc.getControl().forUser(anonymousProvider.get()).isVisible(dbProvider.get())) {
+      throws OrmException, PermissionBackendException {
+    if (!permissionBackend
+        .user(anonymousProvider)
+        .change(rsrc.getNotes())
+        .database(dbProvider)
+        .test(ChangePermission.READ)) {
       return fail(
           reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 9ef445d..5724941 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeOpRepoManager;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.update.UpdateException;
@@ -83,7 +84,8 @@
 
   @Override
   public BinaryResult apply(RevisionResource rsrc)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (Strings.isNullOrEmpty(format)) {
       throw new BadRequestException("format is not specified");
     }
@@ -111,7 +113,8 @@
   }
 
   private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     ReviewDb db = dbProvider.get();
     ChangeControl control = rsrc.getControl();
     IdentifiedUser caller = control.getUser().asIdentifiedUser();
@@ -131,7 +134,8 @@
         | UpdateException
         | IOException
         | ConfigInvalidException
-        | RuntimeException e) {
+        | RuntimeException
+        | PermissionBackendException e) {
       op.close();
       throw e;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 3dd467a..81f830c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -217,7 +217,8 @@
   }
 
   public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input)
-      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException {
+      throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException,
+          PermissionBackendException {
     Change change = rsrc.getChange();
     if (!change.getStatus().isOpen()) {
       throw new ResourceConflictException("change is " + ChangeUtil.status(change));
@@ -340,7 +341,7 @@
     ChangeSet cs;
     try {
       cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getControl().getUser());
-    } catch (OrmException | IOException e) {
+    } catch (OrmException | IOException | PermissionBackendException e) {
       throw new OrmRuntimeException(
           "Could not determine complete set of changes to be submitted", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
index 568b50a..ea53dc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.change.WalkSorter.PatchSetData;
 import com.google.gerrit.server.git.ChangeSet;
 import com.google.gerrit.server.git.MergeSuperSet;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -107,7 +108,7 @@
   @Override
   public Object apply(ChangeResource resource)
       throws AuthException, BadRequestException, ResourceConflictException, IOException,
-          OrmException {
+          OrmException, PermissionBackendException {
     SubmittedTogetherInfo info = applyInfo(resource);
     if (options.isEmpty()) {
       return info.changes;
@@ -116,7 +117,7 @@
   }
 
   public SubmittedTogetherInfo applyInfo(ChangeResource resource)
-      throws AuthException, IOException, OrmException {
+      throws AuthException, IOException, OrmException, PermissionBackendException {
     Change c = resource.getChange();
     try {
       List<ChangeData> cds;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index b79f137..318c251 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -53,6 +53,7 @@
 import com.google.gerrit.server.data.RefUpdateAttribute;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gwtorm.server.OrmException;
@@ -261,7 +262,7 @@
       event.oldAssignee = accountAttributeSupplier(ev.getOldAssignee());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -277,7 +278,7 @@
       event.oldTopic = ev.getOldTopic();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -295,7 +296,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -315,7 +316,7 @@
           approvalsAttributeSupplier(change, ev.getNewApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -333,7 +334,7 @@
         event.reviewer = accountAttributeSupplier(reviewer);
         dispatcher.get().postEvent(change, event);
       }
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -360,7 +361,7 @@
       event.removed = hashtagArray(ev.getRemovedHashtags());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -399,7 +400,7 @@
       event.uploader = accountAttributeSupplier(ev.getWho());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -419,7 +420,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -437,7 +438,7 @@
       event.reason = ev.getReason();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -455,7 +456,7 @@
       event.newRev = ev.getNewRevisionId();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -473,7 +474,7 @@
       event.reason = ev.getReason();
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
@@ -493,7 +494,7 @@
       event.approvals = approvalsAttributeSupplier(change, ev.getApprovals(), ev.getOldApprovals());
 
       dispatcher.get().postEvent(change, event);
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Failed to dispatch event", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index ea28fa9..9d2286e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -63,6 +63,7 @@
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -423,6 +424,7 @@
    * @param submitInput parameters regarding the merge
    * @throws OrmException an error occurred reading or writing the database.
    * @throws RestApiException if an error occurred.
+   * @throws PermissionBackendException if permissions can't be checked
    */
   public void merge(
       ReviewDb db,
@@ -431,7 +433,8 @@
       boolean checkSubmitRules,
       SubmitInput submitInput,
       boolean dryrun)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     this.submitInput = submitInput;
     this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
     this.dryrun = dryrun;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index e880543..72e422b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -37,6 +37,9 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo;
 import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
@@ -101,6 +104,7 @@
   private final ChangeData.Factory changeDataFactory;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<MergeOpRepoManager> repoManagerProvider;
+  private final PermissionBackend permissionBackend;
   private final Config cfg;
   private final Map<QueryKey, List<ChangeData>> queryCache;
   private final Map<Branch.NameKey, Optional<RevCommit>> heads;
@@ -115,13 +119,15 @@
       Accounts accounts,
       ChangeData.Factory changeDataFactory,
       Provider<InternalChangeQuery> queryProvider,
-      Provider<MergeOpRepoManager> repoManagerProvider) {
+      Provider<MergeOpRepoManager> repoManagerProvider,
+      PermissionBackend permissionBackend) {
     this.cfg = cfg;
     this.accountCache = accountCache;
     this.accounts = accounts;
     this.changeDataFactory = changeDataFactory;
     this.queryProvider = queryProvider;
     this.repoManagerProvider = repoManagerProvider;
+    this.permissionBackend = permissionBackend;
     queryCache = new HashMap<>();
     heads = new HashMap<>();
   }
@@ -134,11 +140,13 @@
   }
 
   public ChangeSet completeChangeSet(ReviewDb db, Change change, CurrentUser user)
-      throws IOException, OrmException {
+      throws IOException, OrmException, PermissionBackendException {
     try {
       ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
       cd.changeControl(user);
-      ChangeSet cs = new ChangeSet(cd, cd.changeControl().isVisible(db, cd));
+      ChangeSet cs =
+          new ChangeSet(
+              cd, permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ));
       if (Submit.wholeTopicEnabled(cfg)) {
         return completeChangeSetIncludingTopics(db, cs, user);
       }
@@ -212,7 +220,7 @@
   }
 
   private ChangeSet completeChangeSetWithoutTopic(ReviewDb db, ChangeSet changes, CurrentUser user)
-      throws IOException, OrmException {
+      throws IOException, OrmException, PermissionBackendException {
     Collection<ChangeData> visibleChanges = new ArrayList<>();
     Collection<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -231,7 +239,7 @@
                 + " at ChangeData creation time");
 
         boolean visible = changes.ids().contains(cd.getId());
-        if (visible && !cd.changeControl().isVisible(db, cd)) {
+        if (visible && !canRead(db, user, cd)) {
           // We thought the change was visible, but it isn't.
           // This can happen if the ACL changes during the
           // completeChangeSet computation, for example.
@@ -357,7 +365,7 @@
       CurrentUser user,
       Set<String> topicsSeen,
       Set<String> visibleTopicsSeen)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
     List<ChangeData> visibleChanges = new ArrayList<>();
     List<ChangeData> nonVisibleChanges = new ArrayList<>();
 
@@ -370,7 +378,7 @@
       for (ChangeData topicCd : query().byTopicOpen(topic)) {
         try {
           topicCd.changeControl(user);
-          if (topicCd.changeControl().isVisible(db, topicCd)) {
+          if (canRead(db, user, topicCd)) {
             visibleChanges.add(topicCd);
           } else {
             nonVisibleChanges.add(topicCd);
@@ -402,7 +410,8 @@
   }
 
   private ChangeSet completeChangeSetIncludingTopics(
-      ReviewDb db, ChangeSet changes, CurrentUser user) throws IOException, OrmException {
+      ReviewDb db, ChangeSet changes, CurrentUser user)
+      throws IOException, OrmException, PermissionBackendException {
     Set<String> topicsSeen = new HashSet<>();
     Set<String> visibleTopicsSeen = new HashSet<>();
     int oldSeen;
@@ -443,4 +452,9 @@
     logError(msg);
     throw new OrmException(msg);
   }
+
+  private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
+      throws PermissionBackendException {
+    return permissionBackend.user(user).change(cd).database(db).test(ChangePermission.READ);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 42fb1b3..40120df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -819,7 +819,8 @@
           | OrmException
           | UpdateException
           | IOException
-          | ConfigInvalidException e) {
+          | ConfigInvalidException
+          | PermissionBackendException e) {
         logError("Error submitting changes to " + project.getName(), e);
         reject(magicBranchCmd, "error during submit");
       }
@@ -2264,7 +2265,8 @@
   }
 
   private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace)
-      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException {
+      throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size());
     for (CreateRequest r : create) {
       checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
index 93aa361..2afea25 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -30,8 +30,10 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -262,12 +264,16 @@
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(db.get(), project.getNameKey())) {
-        if (projectCtl.controlForIndexedChange(cd.change()).isVisible(db.get(), cd)) {
+        if (permissionBackend
+            .user(user)
+            .indexedChange(cd, changeNotesFactory.createFromIndexedChange(cd.change()))
+            .database(db)
+            .test(ChangePermission.READ)) {
           visibleChanges.put(cd.getId(), cd.change().getDest());
         }
       }
       return visibleChanges;
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error(
           "Cannot load changes for project "
               + project.getName()
@@ -282,12 +288,12 @@
     try {
       Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>();
       for (ChangeNotes cn : changeNotesFactory.scan(git, db.get(), project)) {
-        if (projectCtl.controlFor(cn).isVisible(db.get())) {
+        if (permissionBackend.user(user).change(cn).database(db).test(ChangePermission.READ)) {
           visibleChanges.put(cn.getChangeId(), cn.getChange().getDest());
         }
       }
       return visibleChanges;
-    } catch (IOException | OrmException e) {
+    } catch (IOException | OrmException | PermissionBackendException e) {
       log.error(
           "Cannot load changes for project " + project + ", assuming no changes are visible", e);
       return Collections.emptyMap();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 15a4b13..7bef0a6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectState;
@@ -405,12 +406,12 @@
   }
 
   @Override
-  protected boolean isVisibleTo(Account.Id to) throws OrmException {
-    return projectState == null
-        || projectState
-            .controlFor(args.identifiedUserFactory.create(to))
-            .controlFor(args.db.get(), change)
-            .isVisible(args.db.get());
+  protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
+    return args.permissionBackend
+        .user(args.identifiedUserFactory.create(to))
+        .change(changeData)
+        .database(args.db.get())
+        .test(ChangePermission.READ);
   }
 
   /** Find all users who are authors of any part of this change. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 4e204ce..e569adf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gwtorm.server.OrmException;
@@ -479,7 +480,7 @@
         rcptTo.add(to);
         add(rt, toAddress(to), override);
       }
-    } catch (OrmException e) {
+    } catch (OrmException | PermissionBackendException e) {
       log.error("Error reading database for account: " + to, e);
     }
   }
@@ -487,9 +488,10 @@
   /**
    * @param to account.
    * @throws OrmException
+   * @throws PermissionBackendException
    * @return whether this email is visible to the given account.
    */
-  protected boolean isVisibleTo(Account.Id to) throws OrmException {
+  protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
     return true;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 24f5164..4c6e6753 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -119,7 +119,12 @@
     }
 
     @Override
-    public ForChange change(ChangeNotes cd) {
+    public ForChange change(ChangeNotes notes) {
+      return new FailedChange(message, cause);
+    }
+
+    @Override
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
       return new FailedChange(message, cause);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 93db963..522eccb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -139,6 +139,15 @@
       return ref(notes.getChange().getDest()).change(notes);
     }
 
+    /**
+     * @return instance scoped for the change loaded from index, and its destination ref and
+     *     project. This method should only be used when database access is harmful and potentially
+     *     stale data from the index is acceptable.
+     */
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+      return ref(notes.getChange().getDest()).indexedChange(cd, notes);
+    }
+
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(GlobalOrPluginPermission perm)
         throws AuthException, PermissionBackendException;
@@ -269,6 +278,12 @@
     /** @return instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
+    /**
+     * @return instance scoped to change loaded from index. This method should only be used when
+     *     database access is harmful and potentially stale data from the index is acceptable.
+     */
+    public abstract ForChange indexedChange(ChangeData cd, ChangeNotes notes);
+
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 94f5ebf..d0b7ad4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -136,18 +136,6 @@
       return create(refControl, notesFactory.create(db, project, changeId));
     }
 
-    /**
-     * Create a change control for a change that was loaded from index. This method should only be
-     * used when database access is harmful and potentially stale data from the index is acceptable.
-     *
-     * @param refControl ref control
-     * @param change change loaded from secondary index
-     * @return change control
-     */
-    ChangeControl createForIndexedChange(RefControl refControl, Change change) {
-      return create(refControl, notesFactory.createFromIndexedChange(change));
-    }
-
     ChangeControl create(RefControl refControl, ChangeNotes notes) {
       return new ChangeControl(changeDataFactory, approvalsUtil, refControl, notes, patchSetUtil);
     }
@@ -209,12 +197,12 @@
   }
 
   /** Can this user see this change? */
-  public boolean isVisible(ReviewDb db) throws OrmException {
+  boolean isVisible(ReviewDb db) throws OrmException {
     return isVisible(db, null);
   }
 
   /** Can this user see this change? */
-  public boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
+  private boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
     if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
       return false;
     }
@@ -226,6 +214,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     if (ps != null && ps.isDraft() && !isDraftVisible(db, null)) {
       return false;
     }
@@ -234,6 +223,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ChangeData cd) throws OrmException {
+    // TODO(hiesel) These don't need to be migrated, just remove after support for drafts is removed
     checkArgument(
         cd.getId().equals(ps.getId().getParentKey()), "%s not for change %s", ps, cd.getId());
     if (ps.isDraft() && !isDraftVisible(cd.db(), cd)) {
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 a1ab177..c7e6e83 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
@@ -190,17 +190,6 @@
         controlForRef(change.getDest()), db, change.getProject(), change.getId());
   }
 
-  /**
-   * Create a change control for a change that was loaded from index. This method should only be
-   * used when database access is harmful and potentially stale data from the index is acceptable.
-   *
-   * @param change change loaded from secondary index
-   * @return change control
-   */
-  public ChangeControl controlForIndexedChange(Change change) {
-    return changeControlFactory.createForIndexedChange(controlForRef(change.getDest()), change);
-  }
-
   public ChangeControl controlFor(ChangeNotes notes) {
     return changeControlFactory.create(controlForRef(notes.getChange().getDest()), notes);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 4bb823e..b12db61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -713,6 +713,11 @@
     }
 
     @Override
+    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+      return getProjectControl().controlFor(notes).asForChange(cd, db);
+    }
+
+    @Override
     public void check(RefPermission perm) throws AuthException, PermissionBackendException {
       if (!can(perm)) {
         throw new AuthException(perm.describeForException() + " not permitted");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index fa08f53..15fd190 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -18,6 +18,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.IsVisibleToPredicate;
@@ -29,17 +32,20 @@
   protected final ChangeNotes.Factory notesFactory;
   protected final ChangeControl.GenericFactory changeControl;
   protected final CurrentUser user;
+  protected final PermissionBackend permissionBackend;
 
   public ChangeIsVisibleToPredicate(
       Provider<ReviewDb> db,
       ChangeNotes.Factory notesFactory,
       ChangeControl.GenericFactory changeControlFactory,
-      CurrentUser user) {
+      CurrentUser user,
+      PermissionBackend permissionBackend) {
     super(ChangeQueryBuilder.FIELD_VISIBLETO, describe(user));
     this.db = db;
     this.notesFactory = notesFactory;
     this.changeControl = changeControlFactory;
     this.user = user;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -47,21 +53,35 @@
     if (cd.fastIsVisibleTo(user)) {
       return true;
     }
+    Change change;
     try {
-      Change c = cd.change();
-      if (c == null) {
+      change = cd.change();
+      if (change == null) {
         return false;
       }
-
-      ChangeNotes notes = notesFactory.createFromIndexedChange(c);
-      ChangeControl cc = changeControl.controlFor(notes, user);
-      if (cc.isVisible(db.get(), cd)) {
-        cd.cacheVisibleTo(cc);
-        return true;
-      }
     } catch (NoSuchChangeException e) {
       // Ignored
+      return false;
     }
+
+    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
+    ChangeControl cc = changeControl.controlFor(notes, user);
+    boolean visible;
+    try {
+      visible =
+          permissionBackend
+              .user(user)
+              .indexedChange(cd, notes)
+              .database(db)
+              .test(ChangePermission.READ);
+    } catch (PermissionBackendException e) {
+      throw new OrmException("unable to check permissions", e);
+    }
+    if (visible) {
+      cd.cacheVisibleTo(cc);
+      return true;
+    }
+
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index aecfc42..980b0dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -940,7 +940,7 @@
 
   public Predicate<ChangeData> visibleto(CurrentUser user) {
     return new ChangeIsVisibleToPredicate(
-        args.db, args.notesFactory, args.changeControlGenericFactory, user);
+        args.db, args.notesFactory, args.changeControlGenericFactory, user, args.permissionBackend);
   }
 
   public Predicate<ChangeData> is_visible() throws QueryParseException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
index f4064f5..eeb6d01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
 import com.google.gerrit.server.index.change.IndexedChangeQuery;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryProcessor;
@@ -55,6 +56,7 @@
   private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeNotes.Factory notesFactory;
   private final DynamicMap<ChangeAttributeFactory> attributeFactories;
+  private final PermissionBackend permissionBackend;
 
   static {
     // It is assumed that basic rewrites do not touch visibleto predicates.
@@ -74,7 +76,8 @@
       Provider<ReviewDb> db,
       ChangeControl.GenericFactory changeControlFactory,
       ChangeNotes.Factory notesFactory,
-      DynamicMap<ChangeAttributeFactory> attributeFactories) {
+      DynamicMap<ChangeAttributeFactory> attributeFactories,
+      PermissionBackend permissionBackend) {
     super(
         userProvider,
         limitsFactory,
@@ -88,6 +91,7 @@
     this.changeControlFactory = changeControlFactory;
     this.notesFactory = notesFactory;
     this.attributeFactories = attributeFactories;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
@@ -130,7 +134,8 @@
   protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) {
     return new AndChangeSource(
         pred,
-        new ChangeIsVisibleToPredicate(db, notesFactory, changeControlFactory, userProvider.get()),
+        new ChangeIsVisibleToPredicate(
+            db, notesFactory, changeControlFactory, userProvider.get(), permissionBackend),
         start);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
index e64ab0e..0801447 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -68,13 +69,13 @@
   }
 
   public void addChange(String id, Map<Change.Id, ChangeResource> changes)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     addChange(id, changes, null);
   }
 
   public void addChange(
       String id, Map<Change.Id, ChangeResource> changes, ProjectControl projectControl)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     addChange(id, changes, projectControl, true);
   }
 
@@ -83,7 +84,7 @@
       Map<Change.Id, ChangeResource> changes,
       ProjectControl projectControl,
       boolean useIndex)
-      throws UnloggedFailure, OrmException {
+      throws UnloggedFailure, OrmException, PermissionBackendException {
     List<ChangeControl> matched =
         useIndex ? changeFinder.find(id, currentUser) : changeFromNotesFactory(id, currentUser);
     List<ChangeControl> toAdd = new ArrayList<>(changes.size());
@@ -97,7 +98,12 @@
     for (ChangeControl ctl : matched) {
       if (!changes.containsKey(ctl.getId())
           && inProject(projectControl, ctl.getProject())
-          && (canMaintainServer || ctl.isVisible(db))) {
+          && (canMaintainServer
+              || permissionBackend
+                  .user(currentUser)
+                  .change(ctl.getNotes())
+                  .database(db)
+                  .test(ChangePermission.READ))) {
         toAdd.add(ctl);
       }
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
index 86209fe..821257c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.Index;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -42,7 +43,7 @@
   void addChange(String token) {
     try {
       changeArgumentParser.addChange(token, changes, null, false);
-    } catch (UnloggedFailure | OrmException e) {
+    } catch (UnloggedFailure | OrmException | PermissionBackendException e) {
       writeError("warning", e.getMessage());
     }
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 5ea5bf7..026f9b7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.change.DeleteReviewer;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.ReviewerResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.ChangeArgumentParser;
 import com.google.gerrit.sshd.CommandMetaData;
@@ -79,6 +80,8 @@
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
       throw new IllegalArgumentException("database is down", e);
+    } catch (PermissionBackendException e) {
+      throw new IllegalArgumentException("can't check permissions", e);
     }
   }
 
diff --git a/plugins/replication b/plugins/replication
index fae5360..0721b20 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit fae5360380023e8351f39be3d4effd4bb2cd8906
+Subproject commit 0721b208ad863ff2f2c7fe1c89950dc2b921abaa