From 377a0e34362cb1ce19fe117d1b52927ebf7e447c Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Sun, 5 Feb 2017 21:41:17 +0100 Subject: [PATCH] SONAR-8734 Add "organization" parameter to WS api/projects/bulk_delete --- .../server/project/ws/BulkDeleteAction.java | 41 +++-- .../server/project/ws/ProjectsWsSupport.java | 2 + .../project/ws/BulkDeleteActionTest.java | 140 +++++++++++++----- 3 files changed, 139 insertions(+), 44 deletions(-) diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java index f85cc5761f8..d19503c40f1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/BulkDeleteAction.java @@ -20,33 +20,38 @@ package org.sonar.server.project.ws; import java.util.List; +import java.util.Optional; import javax.annotation.Nullable; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; +import org.sonar.core.permission.GlobalPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.db.MyBatis; import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; import org.sonar.server.component.ComponentCleanerService; import org.sonar.server.user.UserSession; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; public class BulkDeleteAction implements ProjectsWsAction { - private static final String ACTION = "bulk_delete"; - public static final String PARAM_IDS = "ids"; - public static final String PARAM_KEYS = "keys"; + private static final String ACTION = "bulk_delete"; + private static final String PARAM_IDS = "ids"; + private static final String PARAM_KEYS = "keys"; private final ComponentCleanerService componentCleanerService; private final DbClient dbClient; private final UserSession userSession; + private final ProjectsWsSupport support; - public BulkDeleteAction(ComponentCleanerService componentCleanerService, DbClient dbClient, UserSession userSession) { + public BulkDeleteAction(ComponentCleanerService componentCleanerService, DbClient dbClient, UserSession userSession, + ProjectsWsSupport support) { this.componentCleanerService = componentCleanerService; this.dbClient = dbClient; this.userSession = userSession; + this.support = support; } @Override @@ -67,25 +72,39 @@ public class BulkDeleteAction implements ProjectsWsAction { .createParam(PARAM_KEYS) .setDescription("List of project keys to delete") .setExampleValue(KEY_PROJECT_EXAMPLE_001); + + support.addOrganizationParam(action); } @Override public void handle(Request request, Response response) throws Exception { - userSession.checkLoggedIn().checkIsRoot(); + userSession.checkLoggedIn(); + List uuids = request.paramAsStrings(PARAM_IDS); List keys = request.paramAsStrings(PARAM_KEYS); + String orgKey = request.param(ProjectsWsSupport.PARAM_ORGANIZATION); - DbSession dbSession = dbClient.openSession(false); - try { + try (DbSession dbSession = dbClient.openSession(false)) { + Optional org = loadOrganizationByKey(dbSession, orgKey); List projects = searchProjects(dbSession, uuids, keys); - componentCleanerService.delete(dbSession, projects); - } finally { - MyBatis.closeQuietly(dbSession); + projects.stream() + .filter(p -> !org.isPresent() || org.get().getUuid().equals(p.getOrganizationUuid())) + .forEach(p -> componentCleanerService.delete(dbSession, p)); } response.noContent(); } + private Optional loadOrganizationByKey(DbSession dbSession, @Nullable String orgKey) { + if (orgKey == null) { + userSession.checkIsRoot(); + return Optional.empty(); + } + OrganizationDto org = support.getOrganization(dbSession, orgKey); + userSession.checkOrganizationPermission(org.getUuid(), GlobalPermissions.SYSTEM_ADMIN); + return Optional.of(org); + } + private List searchProjects(DbSession dbSession, @Nullable List uuids, @Nullable List keys) { if (uuids != null) { return dbClient.componentDao().selectByUuids(dbSession, uuids); diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsSupport.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsSupport.java index 813cb6d101d..9fa44b65b0d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsSupport.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsSupport.java @@ -19,6 +19,7 @@ */ package org.sonar.server.project.ws; +import org.sonar.api.server.ServerSide; import org.sonar.api.server.ws.WebService; import org.sonar.db.DbClient; import org.sonar.db.DbSession; @@ -26,6 +27,7 @@ import org.sonar.db.organization.OrganizationDto; import static org.sonar.server.ws.WsUtils.checkFoundWithOptional; +@ServerSide public class ProjectsWsSupport { public static final String PARAM_ORGANIZATION = "organization"; diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java index 7e2a2589577..813b8f1ff0c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/BulkDeleteActionTest.java @@ -19,18 +19,18 @@ */ package org.sonar.server.project.ws; -import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.sonar.api.utils.System2; +import org.sonar.core.permission.GlobalPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDbTester; import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; import org.sonar.server.component.ComponentCleanerService; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.UnauthorizedException; @@ -40,62 +40,109 @@ import org.sonar.server.ws.WsTester; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.sonar.server.project.ws.BulkDeleteAction.PARAM_IDS; -import static org.sonar.server.project.ws.BulkDeleteAction.PARAM_KEYS; +import static org.mockito.Mockito.verifyZeroInteractions; public class BulkDeleteActionTest { private static final String ACTION = "bulk_delete"; - private System2 system2 = System2.INSTANCE; - @Rule - public DbTester db = DbTester.create(system2); + public DbTester db = DbTester.create(System2.INSTANCE); @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); + public UserSessionRule userSession = UserSessionRule.standalone(); @Rule public ExpectedException expectedException = ExpectedException.none(); - private ComponentDbTester componentDbTester = new ComponentDbTester(db); private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class); private WsTester ws; private DbClient dbClient = db.getDbClient(); + private BulkDeleteAction underTest = new BulkDeleteAction(componentCleanerService, dbClient, userSession, new ProjectsWsSupport(dbClient)); + private OrganizationDto org1; + private OrganizationDto org2; @Before public void setUp() { - ws = new WsTester(new ProjectsWs( - new BulkDeleteAction( - componentCleanerService, - dbClient, - userSessionRule))); + ws = new WsTester(new ProjectsWs(underTest)); + org1 = db.organizations().insert(); + org2 = db.organizations().insert(); + } + + @Test + public void root_deletes_projects_by_uuids_in_all_organizations() throws Exception { + userSession.logIn().setRoot(); + ComponentDto toDeleteInOrg1 = db.components().insertProject(org1); + ComponentDto toDeleteInOrg2 = db.components().insertProject(org2); + ComponentDto toKeep = db.components().insertProject(org2); + + WsTester.Result result = ws.newPostRequest("api/projects", ACTION) + .setParam("ids", toDeleteInOrg1.uuid() + "," + toDeleteInOrg2.uuid()) + .execute(); + result.assertNoContent(); + + verifyDeleted(toDeleteInOrg1, toDeleteInOrg2); + } + + @Test + public void root_deletes_projects_by_keys_in_all_organizations() throws Exception { + userSession.logIn().setRoot(); + ComponentDto toDeleteInOrg1 = db.components().insertProject(org1); + ComponentDto toDeleteInOrg2 = db.components().insertProject(org2); + ComponentDto toKeep = db.components().insertProject(org2); + + WsTester.Result result = ws.newPostRequest("api/projects", ACTION) + .setParam("keys", toDeleteInOrg1.key() + "," + toDeleteInOrg2.key()) + .execute(); + result.assertNoContent(); + + verifyDeleted(toDeleteInOrg1, toDeleteInOrg2); } @Test - public void delete_projects_by_uuids() throws Exception { - userSessionRule.logIn().setRoot(); - ComponentDto p1 = componentDbTester.insertProject(); - ComponentDto p2 = componentDbTester.insertProject(); + public void projects_that_dont_exist_are_ignored_and_dont_break_bulk_deletion() throws Exception { + userSession.logIn().setRoot(); + ComponentDto toDelete1 = db.components().insertProject(org1); + ComponentDto toDelete2 = db.components().insertProject(org1); - WsTester.Result result = ws.newPostRequest("api/projects", ACTION).setParam(PARAM_IDS, p1.uuid() + "," + p2.uuid()).execute(); + WsTester.Result result = ws.newPostRequest("api/projects", ACTION) + .setParam("keys", toDelete1.key() + ",missing," + toDelete2.key() + ",doesNotExist") + .execute(); result.assertNoContent(); - verifyDeleted(p1, p2); + verifyDeleted(toDelete1, toDelete2); + } + + @Test + public void throw_ForbiddenException_if_organization_administrator_does_not_set_organization_parameter() throws Exception { + userSession.logIn().addOrganizationPermission(org1.getUuid(), GlobalPermissions.SYSTEM_ADMIN); + ComponentDto project = db.components().insertProject(org1); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + ws.newPostRequest("api/projects", ACTION) + .setParam("keys", project.key()) + .execute(); + + verifyNoDeletions(); } @Test - public void delete_projects_by_keys() throws Exception { - userSessionRule.logIn().setRoot(); - ComponentDto p1 = componentDbTester.insertProject(); - ComponentDto p2 = componentDbTester.insertProject(); + public void organization_administrator_deletes_projects_by_keys_in_his_organization() throws Exception { + userSession.logIn().addOrganizationPermission(org1.getUuid(), GlobalPermissions.SYSTEM_ADMIN); + ComponentDto toDelete = db.components().insertProject(org1); + ComponentDto cantBeDeleted = db.components().insertProject(org2); WsTester.Result result = ws.newPostRequest("api/projects", ACTION) - .setParam(PARAM_KEYS, p1.key() + "," + p2.key()).execute(); + .setParam("organization", org1.getKey()) + .setParam("keys", toDelete.key() + "," + cantBeDeleted.key()) + .execute(); result.assertNoContent(); - verifyDeleted(p1, p2); + verifyDeleted(toDelete); } @Test @@ -103,23 +150,50 @@ public class BulkDeleteActionTest { expectedException.expect(UnauthorizedException.class); expectedException.expectMessage("Authentication is required"); - ws.newPostRequest("api/projects", ACTION).setParam(PARAM_IDS, "whatever-the-uuid").execute(); + ws.newPostRequest("api/projects", ACTION) + .setParam("ids", "whatever-the-uuid").execute(); + + verifyNoDeletions(); + } + + @Test + public void throw_ForbiddenException_if_param_organization_is_not_set_and_not_root() throws Exception { + userSession.logIn().setNonRoot(); + + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + + ws.newPostRequest("api/projects", ACTION) + .setParam("ids", "whatever-the-uuid").execute(); + + verifyNoDeletions(); } @Test - public void throw_ForbiddenException_if_not_root_administrator() throws Exception { - userSessionRule.logIn().setNonRoot(); + public void throw_ForbiddenException_if_param_organization_is_set_but_not_organization_administrator() throws Exception { + userSession.logIn(); expectedException.expect(ForbiddenException.class); expectedException.expectMessage("Insufficient privileges"); - ws.newPostRequest("api/projects", ACTION).setParam(PARAM_IDS, "whatever-the-uuid").execute(); + ws.newPostRequest("api/projects", ACTION) + .setParam("organization", org1.getKey()) + .setParam("ids", "whatever-the-uuid") + .execute(); + + verifyNoDeletions(); } private void verifyDeleted(ComponentDto... projects) { - ArgumentCaptor> argument = (ArgumentCaptor>) ((ArgumentCaptor) ArgumentCaptor.forClass(List.class)); - verify(componentCleanerService).delete(any(DbSession.class), argument.capture()); + ArgumentCaptor argument = ArgumentCaptor.forClass(ComponentDto.class); + verify(componentCleanerService, times(projects.length)).delete(any(DbSession.class), argument.capture()); + + for (ComponentDto project : projects) { + assertThat(argument.getAllValues()).extracting(ComponentDto::uuid).contains(project.uuid()); + } + } - assertThat(argument.getValue()).containsOnly(projects); + private void verifyNoDeletions() { + verifyZeroInteractions(componentCleanerService); } } -- 2.39.5