CheckCodeOwner: Also consider fallback code owners
CheckCodeOwner returned isCodeOwner = false for users that are fallback
code owners, which can be very confusing. Fix CheckCodeOwner so that it
returns isCodeOwner = true for fallback code owners and add a new
isFallbackCodeOwner field.
Signed-off-by: Edwin Kempin <ekempin@google.com>
Change-Id: I6d27f4a71063de3755a6d9ca4ac911124de24f0b
diff --git a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
index 4b33e11..ec3cb4f 100644
--- a/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
+++ b/java/com/google/gerrit/plugins/codeowners/api/CodeOwnerCheckInfo.java
@@ -33,7 +33,8 @@
* <li>any code owner config file assigns codeownership to the email for the path (see {@link
* #codeOwnerConfigFilePaths}) or the email is configured as default code owner (see {@link
* CodeOwnerCheckInfo#isDefaultCodeOwner} field) or the email is configured as global code
- * owner (see {@link #isGlobalCodeOwner} field)
+ * owner (see {@link #isGlobalCodeOwner} field) or the user is a fallback code owner (see
+ * {@link #isFallbackCodeOwner} field)
* </ul>
*/
public boolean isCodeOwner;
@@ -54,6 +55,19 @@
public List<String> codeOwnerConfigFilePaths;
/**
+ * Whether the given email is a fallback code owner of the specified path in the branch.
+ *
+ * <p>True if:
+ *
+ * <ul>
+ * <li>the given email is resolvable (see {@link #isResolvable}) and
+ * <li>no code owners are defined for the specified path in the branch and
+ * <li>parent code owners are not ignored and
+ * <li>the user is a fallback code owner according to the configured fallback code owner policy
+ */
+ public boolean isFallbackCodeOwner;
+
+ /**
* Whether the given email is configured as a default code owner.
*
* <p>If the email is configured as default code owner, but the email is not resolvable (see
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
index f9a0655..7167b28 100644
--- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
+++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwner.java
@@ -18,6 +18,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -31,6 +32,7 @@
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.CodeOwners;
+import com.google.gerrit.plugins.codeowners.backend.FallbackCodeOwners;
import com.google.gerrit.plugins.codeowners.backend.OptionalResultWithMessages;
import com.google.gerrit.plugins.codeowners.backend.PathCodeOwners;
import com.google.gerrit.plugins.codeowners.backend.PathCodeOwnersResult;
@@ -38,8 +40,10 @@
import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
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.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.restapi.account.AccountsCollection;
import com.google.inject.Inject;
@@ -130,6 +134,8 @@
List<Path> codeOwnerConfigFilePaths = new ArrayList<>();
AtomicBoolean isCodeOwnershipAssignedToEmail = new AtomicBoolean(false);
AtomicBoolean isDefaultCodeOwner = new AtomicBoolean(false);
+ AtomicBoolean hasRevelantCodeOwnerDefinitions = new AtomicBoolean(false);
+ AtomicBoolean parentCodeOwnersAreIgnored = new AtomicBoolean(false);
codeOwnerConfigHierarchy.visit(
branchResource.getBranchKey(),
ObjectId.fromString(branchResource.getRevision()),
@@ -166,10 +172,16 @@
"found email %s as code owner in %s", email, codeOwnerConfigFilePath));
codeOwnerConfigFilePaths.add(codeOwnerConfigFilePath);
}
+ } else if (codeOwnerResolverProvider
+ .get()
+ .resolvePathCodeOwners(codeOwnerConfig, absolutePath)
+ .hasRevelantCodeOwnerDefinitions()) {
+ hasRevelantCodeOwnerDefinitions.set(true);
}
if (pathCodeOwnersResult.get().ignoreParentCodeOwners()) {
messages.add("parent code owners are ignored");
+ parentCodeOwnersAreIgnored.set(true);
}
return !pathCodeOwnersResult.get().ignoreParentCodeOwners();
@@ -185,11 +197,19 @@
boolean isResolvable = isResolvableResult.get();
messages.addAll(isResolvableResult.messages());
+ boolean isFallbackCodeOwner =
+ !isCodeOwnershipAssignedToEmail.get()
+ && !hasRevelantCodeOwnerDefinitions.get()
+ && !parentCodeOwnersAreIgnored.get()
+ && isFallbackCodeOwner(branchResource.getNameKey());
+
CodeOwnerCheckInfo codeOwnerCheckInfo = new CodeOwnerCheckInfo();
- codeOwnerCheckInfo.isCodeOwner = isCodeOwnershipAssignedToEmail.get() && isResolvable;
+ codeOwnerCheckInfo.isCodeOwner =
+ (isCodeOwnershipAssignedToEmail.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.debugLogs = messages;
@@ -223,6 +243,43 @@
.isPresent();
}
+ private boolean isFallbackCodeOwner(Project.NameKey projectName) {
+ FallbackCodeOwners fallbackCodeOwners =
+ codeOwnersPluginConfiguration.getFallbackCodeOwners(projectName);
+ switch (fallbackCodeOwners) {
+ case NONE:
+ return false;
+ case PROJECT_OWNERS:
+ return isProjectOwner(projectName);
+ case ALL_USERS:
+ return true;
+ }
+ throw new IllegalStateException(
+ String.format(
+ "unknown value %s for fallbackCodeOwners in project %s",
+ fallbackCodeOwners.name(), projectName));
+ }
+
+ private boolean isProjectOwner(Project.NameKey projectName) {
+ try {
+ AccountResource accountResource =
+ accountsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(email));
+ // There is no dedicated project owner permission, but project owners are detected by checking
+ // the permission to write the project config. Only project owners can do this.
+ return permissionBackend
+ .absentUser(accountResource.getUser().getAccountId())
+ .project(projectName)
+ .test(ProjectPermission.WRITE_CONFIG);
+ } catch (PermissionBackendException
+ | ResourceNotFoundException
+ | AuthException
+ | IOException
+ | ConfigInvalidException e) {
+ throw new StorageException(
+ String.format("failed if email %s is owner of project %s", email, projectName.get()), e);
+ }
+ }
+
private OptionalResultWithMessages<Boolean> isResolvable() {
if (email.equals(CodeOwnerResolver.ALL_USERS_WILDCARD)) {
return OptionalResultWithMessages.create(true);
diff --git a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
index f9f8e7b..8d6524b 100644
--- a/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
+++ b/java/com/google/gerrit/plugins/codeowners/testing/CodeOwnerCheckInfoSubject.java
@@ -53,6 +53,14 @@
check("isCodeOwner").that(codeOwnerCheckInfo().isCodeOwner).isFalse();
}
+ public void isFallbackCodeOwner() {
+ check("isFallbackCodeOwner").that(codeOwnerCheckInfo().isFallbackCodeOwner).isTrue();
+ }
+
+ public void isNotFallbackCodeOwner() {
+ check("isFallbackCodeOwner").that(codeOwnerCheckInfo().isFallbackCodeOwner).isFalse();
+ }
+
public void isResolvable() {
check("isResolvable").that(codeOwnerCheckInfo().isResolvable).isTrue();
}
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 b3e26d8..955c142 100644
--- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
+++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerIT.java
@@ -32,6 +32,7 @@
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
+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.TestPathExpressions;
@@ -1028,6 +1029,134 @@
.hasDebugLogsThatDoNotContainAnyOf(String.format("email %s", mdCodeOwner.email()));
}
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void checkFallbackCodeOwner_AllUsers() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsCodeOwners("/foo/", codeOwner);
+
+ // 1. Check for a file to which fallback code owners do not apply because code owners are
+ // defined
+ String path = "/foo/bar/baz.md";
+
+ // 1a. by a code owner
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 1b. by a non code owner
+ checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 2. Check for a file to which fallback code owners apply because no code owners are defined
+ path = "/other/bar/baz.md";
+ checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isFallbackCodeOwner();
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "PROJECT_OWNERS")
+ public void checkFallbackCodeOwner_ProjectOwners() throws Exception {
+ TestAccount codeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+ setAsCodeOwners("/foo/", codeOwner);
+
+ // 1. Check for a file to which fallback code owners do not apply because code owners are
+ // defined
+ String path = "/foo/bar/baz.md";
+
+ // 1a. by a code owner
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, codeOwner.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 1b. by a project owner
+ checkCodeOwnerInfo = checkCodeOwner(path, admin.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 1c. by a non code owner
+ checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 2. Check for a file to which fallback code owners apply because no code owners are defined
+ path = "/other/bar/baz.md";
+
+ // 2b. by a project owner
+ checkCodeOwnerInfo = checkCodeOwner(path, admin.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isFallbackCodeOwner();
+
+ // 2b. by a non project owner
+ checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ public void noFallbackCodeOwnerIfParentCodeOwnersIgnored() throws Exception {
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .ignoreParentCodeOwners()
+ .create();
+
+ // 1. Check for a file to which parent code owners are ignored
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+
+ // 2. Check for a file to which parent code owners are not ignored
+ path = "/other/bar/baz.md";
+ checkCodeOwnerInfo = checkCodeOwner(path, user.email());
+ assertThat(checkCodeOwnerInfo).isCodeOwner();
+ assertThat(checkCodeOwnerInfo).isFallbackCodeOwner();
+ }
+
+ @Test
+ @GerritConfig(name = "plugin.code-owners.fallbackCodeOwners", value = "ALL_USERS")
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void noFallbackCodeOwnerIfNonVisibleRelevantCodeOwnerExists() throws Exception {
+ TestAccount nonVisibleCodeOwner =
+ accountCreator.create(
+ "codeOwner", "codeOwner@example.com", "Code Owner", /* displayName= */ null);
+
+ codeOwnerConfigOperations
+ .newCodeOwnerConfig()
+ .project(project)
+ .branch("master")
+ .folderPath("/foo/")
+ .addCodeOwnerEmail(nonVisibleCodeOwner.email())
+ .create();
+
+ requestScopeOperations.setApiUser(user.id());
+
+ // verify that the account is not visible
+ assertThrows(
+ ResourceNotFoundException.class, () -> gApi.accounts().id(nonVisibleCodeOwner.id().get()));
+
+ // allow user to call the check code owner REST endpoint
+ projectOperations
+ .allProjectsForUpdate()
+ .add(allowCapability("code-owners-" + CheckCodeOwnerCapability.ID).group(REGISTERED_USERS))
+ .update();
+
+ String path = "/foo/bar/baz.md";
+ CodeOwnerCheckInfo checkCodeOwnerInfo = checkCodeOwner(path, admin.email());
+ assertThat(checkCodeOwnerInfo).isNotCodeOwner();
+ assertThat(checkCodeOwnerInfo).isNotFallbackCodeOwner();
+ }
+
private CodeOwnerCheckInfo checkCodeOwner(String path, String email) throws RestApiException {
return checkCodeOwner(path, email, null);
}
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md
index a4be277..a4dc690 100644
--- a/resources/Documentation/rest-api.md
+++ b/resources/Documentation/rest-api.md
@@ -303,6 +303,7 @@
"code_owner_config_file_paths": [
"/OWNERS",
],
+ "is_fallback_code_owner": "false",
"is_default_code_owner": "false",
"is_global_code_owner": "false",
"debug_logs": [
@@ -804,9 +805,10 @@
| Field Name | Description |
| --------------- | ----------- |
-| `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`).
+| `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.
| `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.
| `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.