Add a global capability for the Check Code Owner Config Files REST endpoint With this new capability we can allow service users to check code owner config files. Bug: Google b/392106108 Change-Id: I154e29d9678994e0660809351bda51c5723043dd Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java index 1b931ba..abba114 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFiles.java
@@ -73,6 +73,7 @@ private final Provider<CurrentUser> currentUser; private final PermissionBackend permissionBackend; private final Provider<ListBranches> listBranches; + private final CheckCodeOwnerConfigFilesCapability checkCodeOwnerConfigFilesCapability; private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; private final CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory; private final CodeOwnerConfigValidator codeOwnerConfigValidator; @@ -82,12 +83,14 @@ Provider<CurrentUser> currentUser, PermissionBackend permissionBackend, Provider<ListBranches> listBranches, + CheckCodeOwnerConfigFilesCapability checkCodeOwnerConfigFilesCapability, CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, CodeOwnerConfigScanner.Factory codeOwnerConfigScannerFactory, CodeOwnerConfigValidator codeOwnerConfigValidator) { this.currentUser = currentUser; this.permissionBackend = permissionBackend; this.listBranches = listBranches; + this.checkCodeOwnerConfigFilesCapability = checkCodeOwnerConfigFilesCapability; this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; this.codeOwnerConfigScannerFactory = codeOwnerConfigScannerFactory; this.codeOwnerConfigValidator = codeOwnerConfigValidator; @@ -101,11 +104,21 @@ throw new AuthException("Authentication required"); } - // This REST endpoint requires the caller to be a project owner. - permissionBackend - .currentUser() - .project(projectResource.getNameKey()) - .check(ProjectPermission.WRITE_CONFIG); + // This REST endpoint requires the caller to be a project owner or have the + // checkCodeOwnerConfigFilesCapability. + if (!permissionBackend + .currentUser() + .project(projectResource.getNameKey()) + .test(ProjectPermission.WRITE_CONFIG) + && !permissionBackend + .currentUser() + .test(checkCodeOwnerConfigFilesCapability.getPermission())) { + throw new AuthException( + String.format( + "cannot check code owner config files, must be project owner or have the %s global" + + " capability", + checkCodeOwnerConfigFilesCapability.getDescription())); + } logger.atFine().log( "checking code owner config files for project %s"
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesCapability.java b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesCapability.java new file mode 100644 index 0000000..6b4840b --- /dev/null +++ b/java/com/google/gerrit/plugins/codeowners/restapi/CheckCodeOwnerConfigFilesCapability.java
@@ -0,0 +1,45 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.restapi; + +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.api.access.PluginPermission; +import com.google.gerrit.extensions.config.CapabilityDefinition; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +/** + * Global capability that allows a user to call the {@link CheckCodeOwnerConfigFiles} REST endpoint. + */ +@Singleton +public class CheckCodeOwnerConfigFilesCapability extends CapabilityDefinition { + public static final String ID = "checkCodeOwnerConfigFiles"; + + private final String pluginName; + + @Inject + CheckCodeOwnerConfigFilesCapability(@PluginName String pluginName) { + this.pluginName = pluginName; + } + + @Override + public String getDescription() { + return "Check Code Owner Config Files"; + } + + public PluginPermission getPermission() { + return new PluginPermission(pluginName, ID); + } +}
diff --git a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java index 37b3823..aa93ca0 100644 --- a/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java +++ b/java/com/google/gerrit/plugins/codeowners/restapi/RestApiModule.java
@@ -56,6 +56,10 @@ get(PROJECT_KIND, "code_owners.project_config").to(GetCodeOwnerProjectConfig.class); put(PROJECT_KIND, "code_owners.project_config").to(PutCodeOwnerProjectConfig.class); + + bind(CapabilityDefinition.class) + .annotatedWith(Exports.named(CheckCodeOwnerConfigFilesCapability.ID)) + .to(CheckCodeOwnerConfigFilesCapability.class); post(PROJECT_KIND, "code_owners.check_config").to(CheckCodeOwnerConfigFiles.class); } }
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java index a275287..749318b 100644 --- a/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/api/CheckCodeOwnerConfigFilesIT.java
@@ -15,6 +15,8 @@ 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.block; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static com.google.gerrit.testing.GerritJUnit.assertThrows; @@ -38,6 +40,7 @@ import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfig; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigImportMode; import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigReference; +import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFilesCapability; import com.google.inject.Inject; import java.util.HashMap; import java.util.List; @@ -66,11 +69,49 @@ } @Test - public void requiresCallerToBeProjectOwner() throws Exception { + public void requiresCallerToBeProjectOwnerOrHaveTheCheckCodeOwnerConfigFilesCapability() + throws Exception { requestScopeOperations.setApiUser(user.id()); AuthException authException = assertThrows(AuthException.class, () -> checkCodeOwnerConfigFilesIn(project)); - assertThat(authException).hasMessageThat().isEqualTo("write refs/meta/config not permitted"); + assertThat(authException) + .hasMessageThat() + .isEqualTo( + "cannot check code owner config files, must be project owner or have the Check Code" + + " Owner Config Files global capability"); + } + + @Test + public void projectOwnerCanCheckCodeOwnerConfigFiles() throws Exception { + requestScopeOperations.setApiUser(user.id()); + projectOperations + .project(project) + .forUpdate() + .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + checkCodeOwnerConfigFilesIn(project); // shouldn't throw an AuthException + } + + @Test + public void adminCanCheckCodeOwnerConfigFiles() throws Exception { + requestScopeOperations.setApiUser(admin.id()); + checkCodeOwnerConfigFilesIn(project); // shouldn't throw an AuthException + } + + @Test + public void userThatHasTheCheckCodeOwnerConfigFilesCapabilityCanCheckCodeOwnerConfigFiles() + throws Exception { + projectOperations + .allProjectsForUpdate() + .add( + allowCapability("code-owners-" + CheckCodeOwnerConfigFilesCapability.ID) + .group(REGISTERED_USERS)) + .update(); + + requestScopeOperations.setApiUser(user.id()); + checkCodeOwnerConfigFilesIn(project); // shouldn't throw an AuthException } @Test
diff --git a/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerFilesCapabilityRestIT.java b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerFilesCapabilityRestIT.java new file mode 100644 index 0000000..712cc6a --- /dev/null +++ b/javatests/com/google/gerrit/plugins/codeowners/acceptance/restapi/CheckCodeOwnerFilesCapabilityRestIT.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.plugins.codeowners.acceptance.restapi; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersIT; +import com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFilesCapability; +import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo; +import com.google.gson.reflect.TypeToken; +import com.google.inject.Inject; +import java.util.Map; +import org.junit.Test; + +/** + * Acceptance test for {@link + * com.google.gerrit.plugins.codeowners.restapi.CheckCodeOwnerConfigFilesCapability}. + */ +public class CheckCodeOwnerFilesCapabilityRestIT extends AbstractCodeOwnersIT { + @Inject private ProjectOperations projectOperations; + + @Test + public void listCapabilities() throws Exception { + RestResponse r = adminRestSession.get("/config/server/capabilities"); + r.assertOK(); + Map<String, CapabilityInfo> capabilities = + newGson() + .fromJson(r.getReader(), new TypeToken<Map<String, CapabilityInfo>>() {}.getType()); + CapabilityInfo capabilityInfo = + capabilities.get("code-owners-" + CheckCodeOwnerConfigFilesCapability.ID); + assertThat(capabilityInfo.id) + .isEqualTo("code-owners-" + CheckCodeOwnerConfigFilesCapability.ID); + assertThat(capabilityInfo.name).isEqualTo("Check Code Owner Config Files"); + } + + @Test + public void getAccountCapabilities() throws Exception { + projectOperations + .allProjectsForUpdate() + .add( + allowCapability("code-owners-" + CheckCodeOwnerConfigFilesCapability.ID) + .group(REGISTERED_USERS)) + .update(); + + RestResponse r = adminRestSession.get("/accounts/self/capabilities"); + r.assertOK(); + Map<String, Object> capabilities = + newGson().fromJson(r.getReader(), new TypeToken<Map<String, Object>>() {}.getType()); + assertThat(capabilities) + .containsEntry("code-owners-" + CheckCodeOwnerConfigFilesCapability.ID, true); + } +}
diff --git a/resources/Documentation/rest-api.md b/resources/Documentation/rest-api.md index ad944aa..e17d5ec 100644 --- a/resources/Documentation/rest-api.md +++ b/resources/Documentation/rest-api.md
@@ -106,7 +106,8 @@ Checks/validates the code owner config files in a project. -Requires that the caller is an owner of the project. +Requires that the caller is an owner of the project or has the +link:checkCodeOwnerConfigFiles[Check Code Owner Config Files] global capability. Input options can be set in the request body as a [CheckCodeOwnerConfigFilesInput](#check-code-owner-config-files-input) entity. @@ -1250,6 +1251,13 @@ assigned on the `All-Projects` project in the `Global Capabilities` access section. +### <a id="checkCodeOwnerConfigFiles">Check Code Owner Config Files + +Global capability that allows a user to call the [Check Code Owner Config +Files](#check-code-owner-config-files) REST endpoint. + +Administrators have this capability implicitly assigned. + --- ## <a id="serviceUsers">Service Users