CheckCodeOwner: Handle files owned by all users

If a file was owned by all users, it was incorrectly reported as not
owned by any user. Fix this and also add a new field in the response to
explicitly let the caller know that the file is owned by all users.

Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I808f71f2817965ab83ef6517bab3989a1e540dc8
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
index ec3cb4f..c22449d 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -83,6 +83,9 @@
    */
   public boolean isGlobalCodeOwner;
 
+  /** Whether the the specified path in the branch is owned by all users (aka {@code *}). */
+  public boolean isOwnedByAllUsers;
+
   /** Debug logs that may help to understand why the user is or isn't a code owner. */
   public List<String> debugLogs;
 }
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index 9ab1ed1..64aa0dd 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -133,6 +133,7 @@
     List<String> messages = new ArrayList<>();
     List<Path> codeOwnerConfigFilePaths = new ArrayList<>();
     AtomicBoolean isCodeOwnershipAssignedToEmail = new AtomicBoolean(false);
+    AtomicBoolean isCodeOwnershipAssignedToAllUsers = new AtomicBoolean(false);
     AtomicBoolean isDefaultCodeOwner = new AtomicBoolean(false);
     AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
     AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false);
@@ -159,7 +160,8 @@
               pathCodeOwnersResult.get().getPathCodeOwners().stream()
                   .filter(cor -> cor.email().equals(email))
                   .findAny();
-          if (codeOwnerReference.isPresent()) {
+          if (codeOwnerReference.isPresent()
+              && !CodeOwnerResolver.ALL_USERS_WILDCARD.equals(email)) {
             isCodeOwnershipAssignedToEmail.set(true);
 
             if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
@@ -174,7 +176,31 @@
                       "found email %s as code owner in %s", email, codeOwnerConfigFilePath));
               codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
             }
-          } else if (codeOwnerResolverProvider
+          }
+
+          if (pathCodeOwnersResult.get().getPathCodeOwners().stream()
+              .anyMatch(cor -> cor.email().equals(CodeOwnerResolver.ALL_USERS_WILDCARD))) {
+            isCodeOwnershipAssignedToAllUsers.set(true);
+
+            if (RefNames.isConfigRef(codeOwnerConfig.key().ref())) {
+              messages.add(
+                  String.format(
+                      "found email %s as code owner in default code owner config",
+                      CodeOwnerResolver.ALL_USERS_WILDCARD));
+              isDefaultCodeOwner.set(true);
+            } else {
+              Path codeOwnerConfigFilePath = codeOwners.getFilePath(codeOwnerConfig.key());
+              messages.add(
+                  String.format(
+                      "found email %s as code owner in %s",
+                      CodeOwnerResolver.ALL_USERS_WILDCARD, codeOwnerConfigFilePath));
+              if (!codeOwnerConfigFilePaths.contains(codeOwnerConfigFilePath)) {
+                codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
+              }
+            }
+          }
+
+          if (codeOwnerResolverProvider
               .get()
               .resolvePathCodeOwners(codeOwnerConfig, absolutePath)
               .hasRevelantCodeOwnerDefinitions()) {
@@ -189,31 +215,46 @@
           return !pathCodeOwnersResult.get().ignoreParentCodeOwners();
         });
 
-    boolean isGlobalCodeOwner = isGlobalCodeOwner(branchResource.getNameKey());
-    if (isGlobalCodeOwner) {
+    boolean isGlobalCodeOwner = false;
+
+    if (isGlobalCodeOwner(branchResource.getNameKey(), email)) {
+      isGlobalCodeOwner = true;
       messages.add(String.format("found email %s as global code owner", email));
       isCodeOwnershipAssignedToEmail.set(true);
     }
 
+    if (isGlobalCodeOwner(branchResource.getNameKey(), CodeOwnerResolver.ALL_USERS_WILDCARD)) {
+      isGlobalCodeOwner = true;
+      messages.add(
+          String.format(
+              "found email %s as global code owner", CodeOwnerResolver.ALL_USERS_WILDCARD));
+      isCodeOwnershipAssignedToAllUsers.set(true);
+    }
+
     OptionalResultWithMessages<Boolean> isResolvableResult = isResolvable();
     boolean isResolvable = isResolvableResult.get();
     messages.addAll(isResolvableResult.messages());
 
     boolean isFallbackCodeOwner =
         !isCodeOwnershipAssignedToEmail.get()
+            && !isCodeOwnershipAssignedToAllUsers.get()
             && !hasRevelantCodeOwnerDefinitions.get()
             && !parentCodeOwnersAreIgnored.get()
             && isFallbackCodeOwner(branchResource.getNameKey());
 
     CodeOwnerCheckInfo codeOwnerCheckInfo = new CodeOwnerCheckInfo();
     codeOwnerCheckInfo.isCodeOwner =
-        (isCodeOwnershipAssignedToEmail.get() || isFallbackCodeOwner) && isResolvable;
+        (isCodeOwnershipAssignedToEmail.get()
+                || isCodeOwnershipAssignedToAllUsers.get()
+                || isFallbackCodeOwner)
+            && isResolvable;
     codeOwnerCheckInfo.isResolvable = isResolvable;
     codeOwnerCheckInfo.codeOwnerConfigFilePaths =
         codeOwnerConfigFilePaths.stream().map(Path::toString).collect(toList());
     codeOwnerCheckInfo.isFallbackCodeOwner = isFallbackCodeOwner && isResolvable;
     codeOwnerCheckInfo.isDefaultCodeOwner = isDefaultCodeOwner.get();
     codeOwnerCheckInfo.isGlobalCodeOwner = isGlobalCodeOwner;
+    codeOwnerCheckInfo.isOwnedByAllUsers = isCodeOwnershipAssignedToAllUsers.get();
     codeOwnerCheckInfo.debugLogs = messages;
     return Response.ok(codeOwnerCheckInfo);
   }
@@ -238,7 +279,7 @@
     }
   }
 
-  private boolean isGlobalCodeOwner(Project.NameKey projectName) {
+  private boolean isGlobalCodeOwner(Project.NameKey projectName, String email) {
     return codeOwnersPluginConfiguration.getProjectConfig(projectName).getGlobalCodeOwners()
         .stream()
         .filter(cor -> cor.email().equals(email))
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
index 8d6524b..2d33006 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -89,6 +89,14 @@
     check("isGlobalCodeOwner").that(codeOwnerCheckInfo().isGlobalCodeOwner).isFalse();
   }
 
+  public void isOwnedByAllUsers() {
+    check("isOwnedByAllUsers").that(codeOwnerCheckInfo().isOwnedByAllUsers).isTrue();
+  }
+
+  public void isNotOwnedByAllUsers() {
+    check("isOwnedByAllUsers").that(codeOwnerCheckInfo().isOwnedByAllUsers).isFalse();
+  }
+
   public void hasDebugLogsThatContainAllOf(String... expectedMessages) {
     for (String expectedMessage : expectedMessages) {
       check("debugLogs").that(codeOwnerCheckInfo().debugLogs).contains(expectedMessage);
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 8b56cd5..3be9ea8 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
 import com.google.gerrit.extensions.api.projects.ConfigInput;
 import com.google.gerrit.extensions.client.ProjectState;
 import com.google.gerrit.extensions.restapi.AuthException;
@@ -35,6 +36,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT;
+import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestCodeOwnerConfigCreation;
 import com.google.gerrit.plugins.codeowners.acceptance.testsuite.TestPathExpressions;
 import com.google.gerrit.plugins.codeowners.api.CodeOwnerCheckInfo;
 import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
@@ -55,6 +57,7 @@
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Arrays;
 import org.eclipse.jgit.lib.Repository;
 import org.junit.Before;
 import org.junit.Test;
@@ -138,6 +141,7 @@
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -168,6 +172,7 @@
         .inOrder();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -211,6 +216,7 @@
         .inOrder();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -235,7 +241,7 @@
         .addSecondaryEmail(secondaryEmail)
         .update();
 
-    setAsRootCodeOwner(secondaryEmail);
+    setAsRootCodeOwners(secondaryEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail);
     assertThat(checkCodeOwnerInfo).isCodeOwner();
@@ -245,6 +251,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -254,6 +261,57 @@
   }
 
   @Test
+  public void checkCodeOwner_ownedByAllUsers() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+    setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isOwnedByAllUsers();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+  }
+
+  @Test
+  public void checkCodeOwner_ownedByEmailAndOwnedByAllUsers() throws Exception {
+    TestAccount codeOwner =
+        accountCreator.create(
+            "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+    setAsRootCodeOwners(codeOwner.email(), CodeOwnerResolver.ALL_USERS_WILDCARD);
+
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, codeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo)
+        .hasCodeOwnerConfigFilePathsThat()
+        .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isOwnedByAllUsers();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in %s",
+                codeOwner.email(), getCodeOwnerConfigFilePath(ROOT_PATH)),
+            String.format(
+                "found email %s as code owner in %s",
+                CodeOwnerResolver.ALL_USERS_WILDCARD, getCodeOwnerConfigFilePath(ROOT_PATH)));
+  }
+
+  @Test
   public void checkNonCodeOwner() throws Exception {
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
@@ -261,6 +319,7 @@
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(String.format("resolved to account %s", user.id()));
   }
@@ -269,7 +328,7 @@
   public void checkNonExistingEmail() throws Exception {
     String nonExistingEmail = "non-exiting@example.com";
 
-    setAsRootCodeOwner(nonExistingEmail);
+    setAsRootCodeOwners(nonExistingEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, nonExistingEmail);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
@@ -279,6 +338,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -293,7 +353,7 @@
   public void checkAmbiguousExistingEmail() throws Exception {
     String ambiguousEmail = "ambiguous@example.com";
 
-    setAsRootCodeOwner(ambiguousEmail);
+    setAsRootCodeOwners(ambiguousEmail);
 
     // Add the email to 2 accounts to make it ambiguous.
     addEmail(user.id(), ambiguousEmail);
@@ -307,6 +367,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -328,7 +389,7 @@
       extIdNotes.commit(md);
     }
 
-    setAsRootCodeOwner(orphanedEmail);
+    setAsRootCodeOwners(orphanedEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, orphanedEmail);
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
@@ -338,6 +399,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -368,6 +430,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -398,6 +461,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -431,6 +495,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -452,11 +517,12 @@
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
   }
 
   @Test
   public void checkAllUsersWildcard_ownedByAllUsers() throws Exception {
-    setAsRootCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
+    setAsRootCodeOwners(CodeOwnerResolver.ALL_USERS_WILDCARD);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo =
         checkCodeOwner(ROOT_PATH, CodeOwnerResolver.ALL_USERS_WILDCARD);
@@ -467,6 +533,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -491,6 +558,7 @@
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -500,6 +568,32 @@
   }
 
   @Test
+  public void checkDefaultCodeOwner_ownedByAllUsers() throws Exception {
+    TestAccount defaultCodeOwner =
+        accountCreator.create(
+            "defaultCodeOwner",
+            "defaultCodeOwner@example.com",
+            "Default Code Owner",
+            /* displayName= */ null);
+    setAsDefaultCodeOwner(CodeOwnerResolver.ALL_USERS_WILDCARD);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, defaultCodeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isOwnedByAllUsers();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as code owner in default code owner config",
+                CodeOwnerResolver.ALL_USERS_WILDCARD),
+            String.format("resolved to account %s", defaultCodeOwner.id()));
+  }
+
+  @Test
   @GerritConfig(name = "plugin.code-owners.globalCodeOwner", value = "globalCodeOwner@example.com")
   public void checkGlobalCodeOwner() throws Exception {
     TestAccount globalCodeOwner =
@@ -516,6 +610,7 @@
     assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format("found email %s as global code owner", globalCodeOwner.email()),
@@ -523,6 +618,33 @@
   }
 
   @Test
+  @GerritConfig(
+      name = "plugin.code-owners.globalCodeOwner",
+      value = CodeOwnerResolver.ALL_USERS_WILDCARD)
+  public void checkGlobalCodeOwner_ownedByAllUsers() throws Exception {
+    TestAccount globalCodeOwner =
+        accountCreator.create(
+            "globalCodeOwner",
+            "globalCodeOwner@example.com",
+            "Global Code Owner",
+            /* displayName= */ null);
+
+    String path = "/foo/bar/baz.md";
+    CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, globalCodeOwner.email());
+    assertThat(checkCodeOwnerInfo).isCodeOwner();
+    assertThat(checkCodeOwnerInfo).isResolvable();
+    assertThat(checkCodeOwnerInfo).hasCodeOwnerConfigFilePathsThat().isEmpty();
+    assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
+    assertThat(checkCodeOwnerInfo).isGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isOwnedByAllUsers();
+    assertThat(checkCodeOwnerInfo)
+        .hasDebugLogsThatContainAllOf(
+            String.format(
+                "found email %s as global code owner", CodeOwnerResolver.ALL_USERS_WILDCARD),
+            String.format("resolved to account %s", globalCodeOwner.id()));
+  }
+
+  @Test
   public void checkCodeOwnerForOtherUser() throws Exception {
     TestAccount codeOwner =
         accountCreator.create(
@@ -538,6 +660,7 @@
         .containsExactly(getCodeOwnerConfigFilePath("/foo/"));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -576,6 +699,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -598,7 +722,7 @@
         .addSecondaryEmail(secondaryEmail)
         .update();
 
-    setAsRootCodeOwner(secondaryEmail);
+    setAsRootCodeOwners(secondaryEmail);
 
     CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(ROOT_PATH, secondaryEmail, user.email());
     assertThat(checkCodeOwnerInfo).isNotCodeOwner();
@@ -608,6 +732,7 @@
         .containsExactly(getCodeOwnerConfigFilePath(ROOT_PATH));
     assertThat(checkCodeOwnerInfo).isNotDefaultCodeOwner();
     assertThat(checkCodeOwnerInfo).isNotGlobalCodeOwner();
+    assertThat(checkCodeOwnerInfo).isNotOwnedByAllUsers();
     assertThat(checkCodeOwnerInfo)
         .hasDebugLogsThatContainAllOf(
             String.format(
@@ -1193,11 +1318,22 @@
     return folderPath + getCodeOwnerConfigFileName();
   }
 
-  private void setAsRootCodeOwner(String email) {
+  private void setAsRootCodeOwners(String... emails) {
+    TestCodeOwnerConfigCreation.Builder builder =
+        codeOwnerConfigOperations
+            .newCodeOwnerConfig()
+            .project(project)
+            .branch("master")
+            .folderPath(ROOT_PATH);
+    Arrays.stream(emails).forEach(builder::addCodeOwnerEmail);
+    builder.create();
+  }
+
+  private void setAsDefaultCodeOwner(String email) {
     codeOwnerConfigOperations
         .newCodeOwnerConfig()
         .project(project)
-        .branch("master")
+        .branch(RefNames.REFS_CONFIG)
         .folderPath(ROOT_PATH)
         .addCodeOwnerEmail(email)
         .create();
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index 29d5fd8..232b619 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -807,6 +807,7 @@
 | `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.
 | `is_global_code_owner` | Whether the given email is configured as a global
 code owner. Note that if the email is configured as global code owner, but the email is not resolvable (see `is_resolvable` field), the user is not a code owner.
+| `is_owned_by_all_users` | Whether the the specified path in the branch is owned by all users (aka `*`).
 | `debug_logs` | List of debug logs that may help to understand why the user is or isn't a code owner.
 
 ---