Allow project deletion on replicas Bug: Issue 15270 Change-Id: Ie6843ca21d96e776b55011a320d5b963e4d35b9c
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java new file mode 100644 index 0000000..8915e78 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionAction.java
@@ -0,0 +1,68 @@ +// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api; + +import com.google.gerrit.extensions.api.access.PluginPermission; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.project.ProjectResource; +import com.google.inject.Inject; +import com.googlesource.gerrit.plugins.replication.LocalFS; +import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps; +import java.util.Optional; +import org.eclipse.jgit.transport.URIish; + +class ProjectDeletionAction + implements RestModifyView<ProjectResource, ProjectDeletionAction.DeleteInput> { + private static final PluginPermission DELETE_PROJECT = + new PluginPermission("delete-project", "deleteProject"); + + static class DeleteInput {} + + private final GerritConfigOps gerritConfigOps; + private final PermissionBackend permissionBackend; + + @Inject + ProjectDeletionAction(GerritConfigOps gerritConfigOps, PermissionBackend permissionBackend) { + this.gerritConfigOps = gerritConfigOps; + this.permissionBackend = permissionBackend; + } + + @Override + public Response<?> apply(ProjectResource projectResource, DeleteInput input) + throws AuthException, BadRequestException, ResourceConflictException, Exception { + + permissionBackend.user(projectResource.getUser()).check(DELETE_PROJECT); + + Optional<URIish> maybeRepoURI = + gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName())); + + if (maybeRepoURI.isPresent()) { + if (new LocalFS(maybeRepoURI.get()).deleteProject(projectResource.getNameKey())) { + return Response.ok(); + } + throw new UnprocessableEntityException( + String.format("Could not delete project %s", projectResource.getName())); + } + throw new ResourceNotFoundException( + String.format("Could not compute URI for repo: %s", projectResource.getName())); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java index a4ea953..44dd3bb 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -76,6 +76,7 @@ private ApplyObjectAction applyObjectAction; private ProjectInitializationAction projectInitializationAction; private UpdateHeadAction updateHEADAction; + private ProjectDeletionAction projectDeletionAction; private ProjectsCollection projectsCollection; private Gson gson; private Provider<CurrentUser> userProvider; @@ -86,12 +87,14 @@ ApplyObjectAction applyObjectAction, ProjectInitializationAction projectInitializationAction, UpdateHeadAction updateHEADAction, + ProjectDeletionAction projectDeletionAction, ProjectsCollection projectsCollection, Provider<CurrentUser> userProvider) { this.fetchAction = fetchAction; this.applyObjectAction = applyObjectAction; this.projectInitializationAction = projectInitializationAction; this.updateHEADAction = updateHEADAction; + this.projectDeletionAction = projectDeletionAction; this.projectsCollection = projectsCollection; this.userProvider = userProvider; this.gson = OutputFormat.JSON.newGsonBuilder().create(); @@ -135,6 +138,12 @@ } else { httpResponse.sendError(SC_UNAUTHORIZED); } + } else if (isDeleteProjectAction(httpRequest)) { + if (userProvider.get().isIdentifiedUser()) { + writeResponse(httpResponse, doDeleteProject(httpRequest)); + } else { + httpResponse.sendError(SC_UNAUTHORIZED); + } } else { chain.doFilter(request, response); } @@ -195,6 +204,14 @@ } @SuppressWarnings("unchecked") + private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception { + ProjectResource projectResource = + projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest)); + return (Response<String>) + projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput()); + } + + @SuppressWarnings("unchecked") private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest) throws IOException, RestApiException, PermissionBackendException { Input input = readJson(httpRequest, TypeLiteral.get(Input.class)); @@ -284,4 +301,9 @@ return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+/HEAD") && "PUT".equals(httpRequest.getMethod()); } + + private boolean isDeleteProjectAction(HttpServletRequest httpRequest) { + return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+$") + && "DELETE".equals(httpRequest.getMethod()); + } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java index 14f8dfb..343ce66 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -48,6 +48,7 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.protocol.HttpClientContext; @@ -124,6 +125,12 @@ return put; } + protected HttpDelete createDeleteRequest() { + HttpDelete delete = new HttpDelete(url); + delete.addHeader(new BasicHeader("Content-Type", "application/json")); + return delete; + } + protected String createRef() throws Exception { return createRef(Project.nameKey(project + TEST_REPLICATION_SUFFIX)); }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java new file mode 100644 index 0000000..4c09266 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -0,0 +1,101 @@ +// Copyright (C) 2021 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.googlesource.gerrit.plugins.replication.pull.api; + +import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability; + +import com.google.gerrit.acceptance.config.GerritConfig; +import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; +import com.google.gerrit.extensions.restapi.Url; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.inject.Inject; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; + +public class ProjectDeletionActionIT extends ActionITBase { + public static final String INVALID_TEST_PROJECT_NAME = "\0"; + public static final String DELETE_PROJECT_PERMISSION = "delete-project-deleteProject"; + + @Inject private ProjectOperations projectOperations; + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception { + httpClientFactory + .create(source) + .execute( + createDeleteRequest(), + assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED), + getAnonymousContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldReturnOKWhenProjectIsDeletedOnReplica() throws Exception { + String testProjectName = project.get(); + url = getURL(testProjectName); + + httpClientFactory + .create(source) + .execute( + createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_OK), getContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilitiesAndNodeIsAReplica() + throws Exception { + String testProjectName = project.get(); + url = getURL(testProjectName); + httpClientFactory + .create(source) + .execute( + createDeleteRequest(), + assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN), + getUserContext()); + + projectOperations + .project(allProjects) + .forUpdate() + .add(allowCapability(DELETE_PROJECT_PERMISSION).group(SystemGroupBackend.REGISTERED_USERS)) + .update(); + + httpClientFactory + .create(source) + .execute( + createDeleteRequest(), + assertHttpResponseCode(HttpServletResponse.SC_OK), + getUserContext()); + } + + @Test + @GerritConfig(name = "container.replica", value = "true") + public void shouldReturnInternalServerErrorIfProjectCannotBeDeletedWhenNodeIsAReplica() + throws Exception { + url = getURL(INVALID_TEST_PROJECT_NAME); + + httpClientFactory + .create(source) + .execute( + createDeleteRequest(), + assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR), + getContext()); + } + + @Override + protected String getURL(String projectName) { + return String.format("%s/a/projects/%s", adminRestSession.url(), Url.encode(projectName)); + } +}