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));
+ }
+}