CheckCodeOwner: Also check permissions of the code owner

Code owners that cannot see the change are filtered out from the
suggestion. Include information that makes detecting this case easy.

While we are at it also check if the code owner has permissions to
code-owner approve the change.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I8939ba7140123e49946c378fef40135975e0b594
diff --git a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
index 0508bc7..1155e35 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/BranchCodeOwners.java
@@ -96,6 +96,7 @@
   abstract class CodeOwnerCheckRequest {
     private String email;
     private String path;
+    private String change;
     private String user;
 
     /**
@@ -131,6 +132,24 @@
     }
 
     /**
+     * Sets the change for which permissions should be checked.
+     *
+     * <p>If not specified change permissions are not checked.
+     *
+     * @param change the change for which permissions should be checked
+     */
+    public CodeOwnerCheckRequest change(@Nullable String change) {
+      this.change = change;
+      return this;
+    }
+
+    /** Returns the change for which permissions should be checked. */
+    @Nullable
+    public String getChange() {
+      return change;
+    }
+
+    /**
      * Sets the user for which the code owner visibility should be checked.
      *
      * <p>If not specified the code owner visibility is not checked.
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
index c22449d..c96645c 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -46,6 +46,51 @@
   public boolean isResolvable;
 
   /**
+   * Whether the user to which the given email was resolved has read permissions on the branch.
+   *
+   * <p>Not set if:
+   *
+   * <ul>
+   *   <li>the given email is not resolvable
+   *   <li>the given email is the all users wildcard (aka {@code *})
+   * </ul>
+   */
+  public Boolean canReadRef;
+
+  /**
+   * Whether the user to which the given email was resolved can see the specified change.
+   *
+   * <p>Not set if:
+   *
+   * <ul>
+   *   <li>the given email is not resolvable
+   *   <li>the given email is the all users wildcard (aka {@code *})
+   *   <li>no change was specified
+   * </ul>
+   */
+  public Boolean canSeeChange;
+
+  /**
+   * Whether the user to which the given email was resolved can code-owner approve the specified
+   * change.
+   *
+   * <p>Being able to code-owner approve the change means that the user has permissions to vote on
+   * the label that is required as code owner approval. Other permissions are not considered for
+   * computing this flag. In particular missing read permissions on the change don't have any effect
+   * on this flag. Whether the user misses read permissions on the change (and hence cannot apply
+   * the code owner approval) can be seen from the {@link #canSeeChange} flag.
+   *
+   * <p>Not set if:
+   *
+   * <ul>
+   *   <li>the given email is not resolvable
+   *   <li>the given email is the all users wildcard (aka {@code *})
+   *   <li>no change was specified
+   * </ul>
+   */
+  public Boolean canApproveChange;
+
+  /**
    * Paths of the code owner config files that assign code ownership to the given email for the
    * specified path.
    *
diff --git a/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java b/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
index 231d591..0bf1917 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/impl/BranchCodeOwnersImpl.java
@@ -99,6 +99,7 @@
         CheckCodeOwner checkCodeOwner = checkCodeOwnerProvider.get();
         checkCodeOwner.setEmail(getEmail());
         checkCodeOwner.setPath(getPath());
+        checkCodeOwner.setChange(getChange());
         checkCodeOwner.setUser(getUser());
         try {
           return checkCodeOwner.apply(branchResource).value();
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index 64aa0dd..f4e5c0f 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -38,12 +38,18 @@
 import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
 import com.google.gerrit.plugins.codeowners.backend.UnresolvedImportFormatter;
 import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+import com.google.gerrit.plugins.codeowners.backend.config.RequiredApproval;
 import com.google.gerrit.plugins.codeowners.util.JgitPath;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.change.ChangeFinder;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.LabelPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.project.BranchResource;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.inject.Inject;
@@ -74,9 +80,12 @@
   private final CodeOwners codeOwners;
   private final AccountsCollection accountsCollection;
   private final UnresolvedImportFormatter unresolvedImportFormatter;
+  private final ChangeFinder changeFinder;
 
   private String email;
   private String path;
+  private String change;
+  private ChangeNotes changeNotes;
   private String user;
   private IdentifiedUser identifiedUser;
 
@@ -90,7 +99,8 @@
       Provider<CodeOwnerResolver> codeOwnerResolverProvider,
       CodeOwners codeOwners,
       AccountsCollection accountsCollection,
-      UnresolvedImportFormatter unresolvedImportFormatter) {
+      UnresolvedImportFormatter unresolvedImportFormatter,
+      ChangeFinder changeFinder) {
     this.checkCodeOwnerCapability = checkCodeOwnerCapability;
     this.permissionBackend = permissionBackend;
     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
@@ -100,6 +110,7 @@
     this.codeOwners = codeOwners;
     this.accountsCollection = accountsCollection;
     this.unresolvedImportFormatter = unresolvedImportFormatter;
+    this.changeFinder = changeFinder;
   }
 
   @Option(name = "--email", usage = "email for which the code ownership should be checked")
@@ -113,6 +124,15 @@
   }
 
   @Option(
+      name = "--change",
+      usage =
+          "change for which permissions should be checked,"
+              + " if not specified change permissions are not checked")
+  public void setChange(String change) {
+    this.change = change;
+  }
+
+  @Option(
       name = "--user",
       usage =
           "user for which the code owner visibility should be checked,"
@@ -127,7 +147,7 @@
           PermissionBackendException {
     permissionBackend.currentUser().check(checkCodeOwnerCapability.getPermission());
 
-    validateInput();
+    validateInput(branchResource);
 
     Path absolutePath = JgitPath.of(path).getAsAbsolutePath();
     List<String> messages = new ArrayList<>();
@@ -231,9 +251,36 @@
       isCodeOwnershipAssignedToAllUsers.set(true);
     }
 
-    OptionalResultWithMessages<Boolean> isResolvableResult = isResolvable();
-    boolean isResolvable = isResolvableResult.get();
-    messages.addAll(isResolvableResult.messages());
+    boolean isResolvable;
+    Boolean canReadRef = null;
+    Boolean canSeeChange = null;
+    Boolean canApproveChange = null;
+    if (email.equals(CodeOwnerResolver.ALL_USERS_WILDCARD)) {
+      isResolvable = true;
+    } else {
+      OptionalResultWithMessages<CodeOwner> isResolvableResult = isResolvable();
+      isResolvable = isResolvableResult.isPresent();
+      messages.addAll(isResolvableResult.messages());
+
+      if (isResolvable) {
+        PermissionBackend.WithUser withUser =
+            permissionBackend.absentUser(isResolvableResult.get().accountId());
+        canReadRef = withUser.ref(branchResource.getBranchKey()).test(RefPermission.READ);
+
+        if (changeNotes != null) {
+          PermissionBackend.ForChange forChange = withUser.change(changeNotes);
+          canSeeChange = forChange.test(ChangePermission.READ);
+          RequiredApproval requiredApproval =
+              codeOwnersPluginConfiguration
+                  .getProjectConfig(branchResource.getNameKey())
+                  .getRequiredApproval();
+          canApproveChange =
+              forChange.test(
+                  new LabelPermission.WithValue(
+                      requiredApproval.labelType(), requiredApproval.value()));
+        }
+      }
+    }
 
     boolean isFallbackCodeOwner =
         !isCodeOwnershipAssignedToEmail.get()
@@ -249,6 +296,9 @@
                 || isFallbackCodeOwner)
             && isResolvable;
     codeOwnerCheckInfo.isResolvable = isResolvable;
+    codeOwnerCheckInfo.canReadRef = canReadRef;
+    codeOwnerCheckInfo.canSeeChange = canSeeChange;
+    codeOwnerCheckInfo.canApproveChange = canApproveChange;
     codeOwnerCheckInfo.codeOwnerConfigFilePaths =
         codeOwnerConfigFilePaths.stream().map(Path::toString).collect(toList());
     codeOwnerCheckInfo.isFallbackCodeOwner = isFallbackCodeOwner && isResolvable;
@@ -259,8 +309,9 @@
     return Response.ok(codeOwnerCheckInfo);
   }
 
-  private void validateInput()
-      throws BadRequestException, AuthException, IOException, ConfigInvalidException {
+  private void validateInput(BranchResource branchResource)
+      throws BadRequestException, AuthException, IOException, ConfigInvalidException,
+          PermissionBackendException {
     if (email == null) {
       throw new BadRequestException("email required");
     }
@@ -277,6 +328,21 @@
         throw new BadRequestException(String.format("user %s not found", user), e);
       }
     }
+    if (change != null) {
+      Optional<ChangeNotes> changeNotes = changeFinder.findOne(change);
+      if (!changeNotes.isPresent()
+          || !permissionBackend
+              .currentUser()
+              .change(changeNotes.get())
+              .test(ChangePermission.READ)) {
+        throw new BadRequestException(String.format("change %s not found", change));
+      }
+      if (!changeNotes.get().getChange().getDest().equals(branchResource.getBranchKey())) {
+        throw new BadRequestException(
+            "target branch of specified change must match branch from the request URL");
+      }
+      this.changeNotes = changeNotes.get();
+    }
   }
 
   private boolean isGlobalCodeOwner(Project.NameKey projectName, String email) {
@@ -324,11 +390,7 @@
     }
   }
 
-  private OptionalResultWithMessages<Boolean> isResolvable() {
-    if (email.equals(CodeOwnerResolver.ALL_USERS_WILDCARD)) {
-      return OptionalResultWithMessages.create(true);
-    }
-
+  private OptionalResultWithMessages<CodeOwner> isResolvable() {
     CodeOwnerResolver codeOwnerResolver = codeOwnerResolverProvider.get();
     if (identifiedUser != null) {
       codeOwnerResolver.forUser(identifiedUser);
@@ -341,6 +403,9 @@
     List<String> messages = new ArrayList<>();
     messages.add(String.format("trying to resolve email %s", email));
     messages.addAll(resolveResult.messages());
-    return OptionalResultWithMessages.create(resolveResult.isPresent(), messages);
+    if (resolveResult.isPresent()) {
+      return OptionalResultWithMessages.create(resolveResult.get(), messages);
+    }
+    return OptionalResultWithMessages.createEmpty(messages);
   }
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
index 2d33006..791705c 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -69,6 +69,42 @@
     check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isFalse();
   }
 
+  public void canReadRef() {
+    check("canReadRef").that(codeOwnerCheckInfo().canReadRef).isTrue();
+  }
+
+  public void cannotReadRef() {
+    check("canReadRef").that(codeOwnerCheckInfo().canReadRef).isFalse();
+  }
+
+  public void canReadRefNotSet() {
+    check("canReadRef").that(codeOwnerCheckInfo().canReadRef).isNull();
+  }
+
+  public void canSeeChange() {
+    check("canSeeChange").that(codeOwnerCheckInfo().canSeeChange).isTrue();
+  }
+
+  public void cannotSeeChange() {
+    check("canSeeChange").that(codeOwnerCheckInfo().canSeeChange).isFalse();
+  }
+
+  public void canSeeChangeNotSet() {
+    check("canSeeChange").that(codeOwnerCheckInfo().canSeeChange).isNull();
+  }
+
+  public void canApproveChange() {
+    check("canApproveChange").that(codeOwnerCheckInfo().canApproveChange).isTrue();
+  }
+
+  public void cannotApproveChange() {
+    check("canApproveChange").that(codeOwnerCheckInfo().canApproveChange).isFalse();
+  }
+
+  public void canApproveChangeNotSet() {
+    check("canApproveChange").that(codeOwnerCheckInfo().canApproveChange).isNull();
+  }
+
   public IterableSubject hasCodeOwnerConfigFilePathsThat() {
     return check("codeOwnerConfigFilePaths").that(codeOwnerCheckInfo().codeOwnerConfigFilePaths);
   }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
index 238dc9f..0db2388 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.plugins.codeowners.acceptance.api;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
 import static com.google.gerrit.plugins.codeowners.testing.CodeOwnerCheckInfoSubject.assertThat;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
@@ -27,8 +30,10 @@
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Permission;
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.projects.BranchInput;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -130,6 +135,9 @@
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
     assertThat(checkCodeOwnerInfo).isCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).canReadRef();
+    assertThat(checkCodeOwnerInfo).canSeeChangeNotSet();
+    assertThat(checkCodeOwnerInfo).canApproveChangeNotSet();
     assertThat(checkCodeOwnerInfo)
         .hasCodeOwnerConfigFilePathsThat()
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
@@ -508,6 +516,9 @@
         checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
     assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).canReadRefNotSet();
+    assertThat(checkCodeOwnerInfo).canSeeChangeNotSet();
+    assertThat(checkCodeOwnerInfo).canApproveChangeNotSet();
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
@@ -1290,8 +1301,150 @@
     assertThat(checkCodeOwnerInfo).hasDebugLogsThatDoNotContainAnyOf("");
   }
 
+  @Test
+  public void checkCodeOwnerThatCannotReadRef() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsCodeOwners("/foo/", codeOwner);
+
+    // Make read permission on master branch exclusive for admins, so that the code owner cannot
+    // read master.
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(adminGroupUuid()))
+        .setExclusiveGroup(permissionKey(Permission.READ).ref("refs/heads/master"), true)
+        .update();
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).cannotReadRef();
+    assertThat(checkCodeOwnerInfo).canSeeChangeNotSet();
+    assertThat(checkCodeOwnerInfo).canApproveChangeNotSet();
+  }
+
+  @Test
+  public void cannotCheckForNonExistingChange() throws Exception {
+    String nonExistingChange = "non-existing";
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class,
+            () -> checkCodeOwnerForChange("/", user.email(), nonExistingChange));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("change %s not found", nonExistingChange));
+  }
+
+  @Test
+  public void cannotCheckForNonVisibleChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).setPrivate(true);
+
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allowCapability("code-owners-" + CheckCodeOwnerCapability.ID).group(REGISTERED_USERS))
+        .update();
+
+    requestScopeOperations.setApiUser(user.id());
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class, () -> checkCodeOwnerForChange("/", user.email(), changeId));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo(String.format("change %s not found", changeId));
+  }
+
+  @Test
+  public void cannotCheckForChangeOfOtherBranch() throws Exception {
+    // Create another branch
+    String branchName = "foo";
+    BranchInput branchInput = new BranchInput();
+    branchInput.ref = branchName;
+    branchInput.revision = projectOperations.project(project).getHead("master").name();
+    gApi.projects().name(project.get()).branch(branchInput.ref).create(branchInput);
+
+    String changeId = createChange("refs/for/" + branchName).getChangeId();
+    BadRequestException exception =
+        assertThrows(
+            BadRequestException.class, () -> checkCodeOwnerForChange("/", user.email(), changeId));
+    assertThat(exception)
+        .hasMessageThat()
+        .isEqualTo("target branch of specified change must match branch from the request URL");
+  }
+
+  @Test
+  public void checkCodeOwnerThatCannotSeeChange_privateChange() throws Exception {
+    String changeId = createChange().getChangeId();
+    gApi.changes().id(changeId).setPrivate(true);
+
+    testCheckCodeOwnerThatCannotSeeChange(changeId, /* canReadRef= */ true);
+  }
+
+  @Test
+  public void checkCodeOwnerThatCannotSeeChange_cannotReadRef() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Make read permission on master branch exclusive for admins, so that the code owner cannot
+    // read master.
+    projectOperations
+        .allProjectsForUpdate()
+        .add(allow(Permission.READ).ref("refs/heads/master").group(adminGroupUuid()))
+        .setExclusiveGroup(permissionKey(Permission.READ).ref("refs/heads/master"), true)
+        .update();
+
+    testCheckCodeOwnerThatCannotSeeChange(changeId, /* canReadRef= */ false);
+  }
+
+  private void testCheckCodeOwnerThatCannotSeeChange(String changeId, boolean canReadRef)
+      throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsCodeOwners("/foo/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwnerForChange(path, codeOwner.email(), changeId);
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    if (canReadRef) {
+      assertThat(checkCodeOwnerInfo).canReadRef();
+    } else {
+      assertThat(checkCodeOwnerInfo).cannotReadRef();
+    }
+    assertThat(checkCodeOwnerInfo).cannotSeeChange();
+    assertThat(checkCodeOwnerInfo).canApproveChange();
+  }
+
+  @Test
+  public void checkCodeOwnerThatCannotApproveChange() throws Exception {
+    String changeId = createChange().getChangeId();
+
+    // Remove permission to vote on the Code-Review label.
+    projectOperations
+        .allProjectsForUpdate()
+        .remove(labelPermissionKey("Code-Review").ref("refs/heads/*"))
+        .update();
+
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+    setAsCodeOwners("/foo/", codeOwner);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo =
+        checkCodeOwnerForChange(path, codeOwner.email(), changeId);
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).canReadRef();
+    assertThat(checkCodeOwnerInfo).canSeeChange();
+    assertThat(checkCodeOwnerInfo).cannotApproveChange();
+  }
+
   private CodeOwnerCheckInfo checkCodeOwner(String path, String email) throws RestApiException {
-    return checkCodeOwner(path, email, null);
+    return checkCodeOwner(path, email, /* user= */ null);
   }
 
   private CodeOwnerCheckInfo checkCodeOwner(String path, String email, @Nullable String user)
@@ -1306,6 +1459,18 @@
         .check();
   }
 
+  private CodeOwnerCheckInfo checkCodeOwnerForChange(
+      String path, String email, @Nullable String change) throws RestApiException {
+    return projectCodeOwnersApiFactory
+        .project(project)
+        .branch("master")
+        .checkCodeOwner()
+        .path(path)
+        .email(email)
+        .change(change)
+        .check();
+  }
+
   private String getCodeOwnerConfigFilePath(String folderPath) {
     assertThat(folderPath).startsWith("/");
     assertThat(folderPath).endsWith("/");
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 20a6e5f..df39f6f 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -268,6 +268,7 @@
 | ----------- | --------- | ----------- |
 | `email`     | mandatory | Email for which the code ownership should be checked.
 | `path`      | mandatory | Path for which the code ownership should be checked.
+| `change`    | optional  | Change for which permissions should be checked. If not specified change permissions are not checked.
 | `user`      | optional  | User for which the code owner visibility should be checked. If not specified the code owner visibility is not checked. Can be used to investigate why a code owner is not shown/suggested to this user.
 
 Requires that the caller has the [Check Code Owner](#checkCodeOwner) or the
@@ -300,6 +301,7 @@
   {
     "is_code_owner": "false",
     "is_resolvable": "false",
+    "can_read_ref": "true",
     "code_owner_config_file_paths": [
       "/OWNERS",
     ],
@@ -823,6 +825,9 @@
 | --------------- | ----------- |
 | `is_code_owner` | Whether the given email owns the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) any code owner config file assigns codeownership to the email for the path (see field `code_owner_config_file_paths`) or the email is configured as default code owner (see field `is_default_code_owner` or the email is configured as global code owner (see field `is_global_code_owner`) or the user is a fallback code owner (see field `is_fallback_code_owner`).
 | `is_resolvable` | Whether the given email is resolvable for the specified user or the calling user if no user was specified.
+| `can_read_ref` | Whether the user to which the given email was resolved has read permissions on the branch. Not set if the given email is not resolvable or if the given email is the all users wildcard (aka '*').
+| `can_see_change`| Whether the user to which the given email was resolved can see the specified change. Not set if the given email is not resolvable, if the given email is the all users wildcard (aka '*') or if no change was specified.
+| `can_approve_change`| Whether the user to which the given email was resolved can code-owner approve the specified change. Being able to code-owner approve the change means that the user has permissions to vote on the label that is [required as code owner approval](config.html#pluginCodeOwnersRequiredApproval). Other permissions are not considered for computing this flag. In particular missing read permissions on the change don't have any effect on this flag. Whether the user misses read permissions on the change (and hence cannot apply the code owner approval) can be seen from the `can_see_change` flag. Not set if the given email is not resolvable, if the given email is the all users wildcard (aka '*') or if no change was specified.
 | `code_owner_config_file_paths` | Paths of the code owner config files that assign code ownership to the specified email and path as a list. Note that if code ownership is assigned to the email via a code owner config files, but the email is not resolvable (see field `is_resolvable` field), the user is not a code owner.
 | `is_fallback_code_owner` | Whether the given email is a fallback code owner of the specified path in the branch. True if: a) the given email is resolvable (see field `is_resolvable') and b) no code owners are defined for the specified path in the branch and c) parent code owners are not ignored and d) the user is a fallback code owner according to the [configured fallback code owner policy](config.html#pluginCodeOwnersFallbackCodeOwners)
 | `is_default_code_owner` | Whether the given email is configured as a default code owner in the code owner config file in `refs/meta/config`. Note that if the email is configured as default code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.