From ef96a1ccca48638bf16aecf941b36b5fe4213bd1 Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Thu, 13 Apr 2017 15:44:51 +0200 Subject: [PATCH] SONAR-9105 add WS api/projects/change_visibility --- .../server/project/ws/ProjectsWsModule.java | 3 +- .../project/ws/UpdateVisibilityAction.java | 152 +++++ .../project/ws/ProjectsWsModuleTest.java | 2 +- .../ws/UpdateVisibilityActionTest.java | 614 ++++++++++++++++++ 4 files changed, 769 insertions(+), 2 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java index 4fcc3bebc44..48ce49927e7 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java @@ -37,6 +37,7 @@ public class ProjectsWsModule extends Module { ProvisionedAction.class, SearchMyProjectsAction.class, SearchMyProjectsDataLoader.class, - SearchAction.class); + SearchAction.class, + UpdateVisibilityAction.class); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java b/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java new file mode 100644 index 00000000000..6f37b1f70e0 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java @@ -0,0 +1,152 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.project.ws; + +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Scopes; +import org.sonar.api.server.ws.Request; +import org.sonar.api.server.ws.Response; +import org.sonar.api.server.ws.WebService; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.permission.GroupPermissionDto; +import org.sonar.db.permission.UserPermissionDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.permission.index.PermissionIndexer; +import org.sonar.server.user.UserSession; + +import static java.util.Collections.singletonList; +import static org.sonar.core.permission.ProjectPermissions.PUBLIC_PERMISSIONS; +import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; +import static org.sonar.server.ws.WsUtils.checkRequest; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_PROJECT; + +public class UpdateVisibilityAction implements ProjectsWsAction { + private static final String ACTION = "update_visibility"; + private static final String PARAM_VISIBILITY = "visibility"; + private static final String PUBLIC_VISIBILITY = "public"; + private static final String PRIVATE_VISIBILITY = "private"; + private static final Set ALLOWED_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.VIEW); + + private final DbClient dbClient; + private final ComponentFinder componentFinder; + private final UserSession userSession; + private final PermissionIndexer permissionIndexer; + + public UpdateVisibilityAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, + PermissionIndexer permissionIndexer) { + this.dbClient = dbClient; + this.componentFinder = componentFinder; + this.userSession = userSession; + this.permissionIndexer = permissionIndexer; + } + + public void define(WebService.NewController context) { + WebService.NewAction action = context.createAction(ACTION) + .setDescription("Updates visibility of a project or a view.
" + + "Requires 'Project administer' permission on the specified project or view") + .setSince("6.4") + .setPost(true) + .setHandler(this); + + action.createParam(PARAM_PROJECT) + .setDescription("Project or view key") + .setExampleValue(KEY_PROJECT_EXAMPLE_001) + .setRequired(true); + + action.createParam(PARAM_VISIBILITY) + .setDescription("new visibility of the project or view") + .setPossibleValues(PUBLIC_VISIBILITY, PRIVATE_VISIBILITY) + .setRequired(true); + } + + @Override + public void handle(Request request, Response response) throws Exception { + userSession.checkLoggedIn(); + + String projectKey = request.mandatoryParam(PARAM_PROJECT); + boolean changeToPrivate = PRIVATE_VISIBILITY.equals(request.mandatoryParam(PARAM_VISIBILITY)); + + try (DbSession dbSession = dbClient.openSession(false)) { + ComponentDto component = componentFinder.getByKey(dbSession, projectKey); + checkRequest(isRoot(component), "Component must either be a project or a view"); + checkRequest(!changeToPrivate || !Qualifiers.VIEW.equals(component.qualifier()), "Views can't be made private"); + userSession.checkComponentPermission(UserRole.ADMIN, component); + checkRequest(noPendingTask(dbSession, component), "Component visibility can't be changed as long as it has background task(s) pending or in progress"); + + if (changeToPrivate != component.isPrivate()) { + dbClient.componentDao().setPrivateForRootComponentUuid(dbSession, component.uuid(), changeToPrivate); + if (changeToPrivate) { + updatePermissionsToPrivate(dbSession, component); + } else { + updatePermissionsToPublic(dbSession, component); + } + dbSession.commit(); + permissionIndexer.indexProjectsByUuids(dbSession, singletonList(component.uuid())); + } + } + } + + private static boolean isRoot(ComponentDto component) { + return Scopes.PROJECT.equals(component.scope()) && ALLOWED_QUALIFIERS.contains(component.qualifier()); + } + + private boolean noPendingTask(DbSession dbSession, ComponentDto rootComponent) { + return dbClient.ceQueueDao().selectByComponentUuid(dbSession, rootComponent.uuid()).isEmpty(); + } + + private void updatePermissionsToPrivate(DbSession dbSession, ComponentDto component) { + // delete project permissions for group AnyOne + dbClient.groupPermissionDao().deleteByRootComponentIdAndGroupId(dbSession, component.getId(), null); + // grant UserRole.CODEVIEWER and UserRole.USER + PUBLIC_PERMISSIONS.forEach(permission -> { + dbClient.groupPermissionDao().selectGroupIdsWithPermissionOnProjectBut(dbSession, component.getId(), permission) + .forEach(groupId -> insertProjectPermissionOnGroup(dbSession, component, permission, groupId)); + dbClient.userPermissionDao().selectUserIdsWithPermissionOnProjectBut(dbSession, component.getId(), permission) + .forEach(userId -> insertProjectPermissionOnUser(dbSession, component, permission, userId)); + }); + } + + private void insertProjectPermissionOnUser(DbSession dbSession, ComponentDto component, String permission, Integer userId) { + dbClient.userPermissionDao().insert(dbSession, new UserPermissionDto(component.getOrganizationUuid(), permission, userId, component.getId())); + } + + private void insertProjectPermissionOnGroup(DbSession dbSession, ComponentDto component, String permission, Integer groupId) { + dbClient.groupPermissionDao().insert(dbSession, new GroupPermissionDto() + .setOrganizationUuid(component.getOrganizationUuid()) + .setResourceId(component.getId()) + .setGroupId(groupId) + .setRole(permission)); + } + + private void updatePermissionsToPublic(DbSession dbSession, ComponentDto component) { + PUBLIC_PERMISSIONS.forEach(permission -> { + // delete project group permission for UserRole.CODEVIEWER and UserRole.USER + dbClient.groupPermissionDao().deleteByRootComponentIdAndPermission(dbSession, component.getId(), permission); + // delete project user permission for UserRole.CODEVIEWER and UserRole.USER + dbClient.userPermissionDao().deleteProjectPermissionOfAnyUser(dbSession, component.getId(), permission); + }); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java index 1a8b29a0624..dd594cda099 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java @@ -30,6 +30,6 @@ public class ProjectsWsModuleTest { public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new ProjectsWsModule().configure(container); - assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 13); + assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 14); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java new file mode 100644 index 00000000000..6e2b392307b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java @@ -0,0 +1,614 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.project.ws; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.api.web.UserRole; +import org.sonar.core.permission.ProjectPermissions; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.ce.CeQueueDto; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.GroupPermissionDto; +import org.sonar.db.permission.OrganizationPermission; +import org.sonar.db.permission.UserPermissionDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.component.ComponentFinder; +import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.exceptions.ForbiddenException; +import org.sonar.server.exceptions.NotFoundException; +import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.permission.index.PermissionIndexer; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestRequest; +import org.sonar.server.ws.WsActionTester; + +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class UpdateVisibilityActionTest { + private static final String PARAM_VISIBILITY = "visibility"; + private static final String PARAM_PROJECT = "project"; + private static final String PUBLIC = "public"; + private static final String PRIVATE = "private"; + private static final Set ORGANIZATION_PERMISSIONS_NAME_SET = stream(OrganizationPermission.values()).map(OrganizationPermission::getKey) + .collect(MoreCollectors.toSet(OrganizationPermission.values().length)); + private static final Set PROJECT_PERMISSIONS_BUT_USER_AND_CODEVIEWER = ProjectPermissions.ALL.stream() + .filter(perm -> !perm.equals(UserRole.USER) && !perm.equals(UserRole.CODEVIEWER)).collect(MoreCollectors.toSet(ProjectPermissions.ALL.size() - 2)); + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone() + .logIn(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private DbClient dbClient = dbTester.getDbClient(); + private DbSession dbSession = dbTester.getSession(); + private PermissionIndexer permissionIndexer = mock(PermissionIndexer.class); + + private UpdateVisibilityAction underTest = new UpdateVisibilityAction(dbClient, new ComponentFinder(dbClient), userSessionRule, permissionIndexer); + private WsActionTester actionTester = new WsActionTester(underTest); + + private final Random random = new Random(); + private final String randomVisibility = random.nextBoolean() ? PUBLIC : PRIVATE; + private final TestRequest request = actionTester.newRequest(); + private final OrganizationDto organization = dbTester.organizations().insert(); + + @Test + public void execute_fails_if_user_is_not_logged_in() { + userSessionRule.anonymous(); + + expectedException.expect(UnauthorizedException.class); + expectedException.expectMessage("Authentication is required"); + + request.execute(); + } + + @Test + public void execute_fails_with_IAE_when_project_parameter_is_not_provided() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The 'project' parameter is missing"); + + request.execute(); + } + + @Test + public void execute_fails_with_IAE_when_project_parameter_is_not_empty() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The 'project' parameter is missing"); + + request.execute(); + } + + @Test + public void execute_fails_with_IAE_when_parameter_visibility_is_not_provided() { + request.setParam(PARAM_PROJECT, "foo"); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("The 'visibility' parameter is missing"); + + request.execute(); + } + + @Test + public void execute_fails_with_IAE_when_parameter_visibility_is_empty() { + request.setParam(PARAM_PROJECT, "foo") + .setParam(PARAM_VISIBILITY, ""); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Value of parameter '" + PARAM_VISIBILITY + "' () must be one of: [public, private]"); + + request.execute(); + } + + @Test + public void execute_fails_with_IAE_when_value_of_parameter_visibility_is_not_lowercase() { + request.setParam(PARAM_PROJECT, "foo"); + + Stream.of("PUBLIC", "pUBliC", "PRIVATE", "PrIVAtE") + .forEach(visibility -> { + try { + request.setParam(PARAM_VISIBILITY, visibility).execute(); + fail("An exception should have been raised"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo(format("Value of parameter '%s' (%s) must be one of: %s", PARAM_VISIBILITY, visibility, "[public, private]")); + } + }); + } + + @Test + public void execute_fails_with_NotFoundException_when_specified_component_does_not_exist() { + request.setParam(PARAM_PROJECT, "foo") + .setParam(PARAM_VISIBILITY, randomVisibility); + + expectedException.expect(NotFoundException.class); + expectedException.expectMessage("Component key 'foo' not found"); + + request.execute(); + } + + @Test + public void execute_fails_with_BadRequestException_if_specified_component_is_neither_a_project_nor_a_view() { + ComponentDto project = randomPublicOrPrivateProject(); + ComponentDto module = ComponentTesting.newModuleDto(project); + ComponentDto dir = ComponentTesting.newDirectory(project, "path"); + ComponentDto file = ComponentTesting.newFileDto(project); + dbTester.components().insertComponents(module, dir, file); + ComponentDto view = dbTester.components().insertView(organization); + ComponentDto subView = ComponentTesting.newSubView(view); + ComponentDto projectCopy = ComponentTesting.newProjectCopy("foo", project, subView); + dbTester.components().insertComponents(subView, projectCopy); + + Stream.of(module, dir, file, subView, projectCopy) + .forEach(nonRootComponent -> { + request.setParam(PARAM_PROJECT, nonRootComponent.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + + try { + request.execute(); + fail("a BadRequestException should have been raised"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo("Component must either be a project or a view"); + } + }); + } + + @Test + public void execute_throws_ForbiddenException_if_user_has_no_permission_on_specified_component() { + ComponentDto project = dbTester.components().insertPrivateProject(organization); + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + + expectInsufficientPrivilegeException(); + + request.execute(); + } + + @Test + public void execute_throws_ForbiddenException_if_user_has_all_permissions_but_ADMIN_on_specified_component() { + ComponentDto project = dbTester.components().insertPublicProject(organization); + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + userSessionRule.addProjectPermission(UserRole.ISSUE_ADMIN, project); + Arrays.stream(OrganizationPermission.values()) + .forEach(perm -> userSessionRule.addPermission(perm, organization)); + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + + expectInsufficientPrivilegeException(); + + request.execute(); + } + + @Test + public void execute_throws_BadRequestException_if_specified_component_has_pending_tasks() { + ComponentDto project = randomPublicOrPrivateProject(); + IntStream.range(0, 1 + Math.abs(random.nextInt(5))) + .forEach(i -> insertPendingTask(project)); + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Component visibility can't be changed as long as it has background task(s) pending or in progress"); + + request.execute(); + } + + @Test + public void execute_throws_BadRequestException_if_specified_component_has_in_progress_tasks() { + ComponentDto project = randomPublicOrPrivateProject(); + IntStream.range(0, 1 + Math.abs(random.nextInt(5))) + .forEach(i -> insertInProgressTask(project)); + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, randomVisibility); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Component visibility can't be changed as long as it has background task(s) pending or in progress"); + + request.execute(); + } + + @Test + public void execute_changes_private_flag_of_specified_project_and_all_children_to_specified_new_visibility() { + ComponentDto project = randomPublicOrPrivateProject(); + boolean initiallyPrivate = project.isPrivate(); + ComponentDto module = ComponentTesting.newModuleDto(project); + ComponentDto dir = ComponentTesting.newDirectory(project, "path"); + ComponentDto file = ComponentTesting.newFileDto(project); + dbTester.components().insertComponents(module, dir, file); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, initiallyPrivate ? PUBLIC : PRIVATE) + .execute(); + + assertThat(isPrivateInDb(project)).isEqualTo(!initiallyPrivate); + assertThat(isPrivateInDb(module)).isEqualTo(!initiallyPrivate); + assertThat(isPrivateInDb(dir)).isEqualTo(!initiallyPrivate); + assertThat(isPrivateInDb(file)).isEqualTo(!initiallyPrivate); + } + + @Test + public void execute_has_no_effect_when_changing_a_view_to_public() { + ComponentDto project = randomPublicOrPrivateProject(); + ComponentDto view = dbTester.components().insertView(organization); + ComponentDto subView = ComponentTesting.newSubView(view); + ComponentDto projectCopy = ComponentTesting.newProjectCopy("foo", project, subView); + dbTester.components().insertComponents(subView, projectCopy); + userSessionRule.addProjectPermission(UserRole.ADMIN, view); + + request.setParam(PARAM_PROJECT, view.key()) + .setParam(PARAM_VISIBILITY, PUBLIC) + .execute(); + + assertThat(isPrivateInDb(view)).isEqualTo(false); + assertThat(isPrivateInDb(subView)).isEqualTo(false); + assertThat(isPrivateInDb(projectCopy)).isEqualTo(false); + } + + @Test + public void execute_fails_with_BadRequestException_when_changing_a_view_to_private() { + ComponentDto project = randomPublicOrPrivateProject(); + ComponentDto view = dbTester.components().insertView(organization); + ComponentDto subView = ComponentTesting.newSubView(view); + ComponentDto projectCopy = ComponentTesting.newProjectCopy("foo", project, subView); + dbTester.components().insertComponents(subView, projectCopy); + userSessionRule.addProjectPermission(UserRole.ADMIN, view); + TestRequest request = this.request.setParam(PARAM_PROJECT, view.key()) + .setParam(PARAM_VISIBILITY, PRIVATE); + + expectedException.expect(BadRequestException.class); + expectedException.expectMessage("Views can't be made private"); + + request.execute(); + } + + @Test + public void execute_has_no_effect_if_specified_project_already_has_specified_visibility() { + ComponentDto project = randomPublicOrPrivateProject(); + boolean initiallyPrivate = project.isPrivate(); + ComponentDto module = ComponentTesting.newModuleDto(project) + .setPrivate(initiallyPrivate); + ComponentDto dir = ComponentTesting.newDirectory(project, "path") + // child is inconsistent with root (should not occur) and won't be fixed + .setPrivate(!initiallyPrivate); + ComponentDto file = ComponentTesting.newFileDto(project) + .setPrivate(initiallyPrivate); + dbTester.components().insertComponents(module, dir, file); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, initiallyPrivate ? PRIVATE : PUBLIC) + .execute(); + + assertThat(isPrivateInDb(project)).isEqualTo(initiallyPrivate); + assertThat(isPrivateInDb(module)).isEqualTo(initiallyPrivate); + assertThat(isPrivateInDb(dir)).isEqualTo(!initiallyPrivate); + assertThat(isPrivateInDb(file)).isEqualTo(initiallyPrivate); + } + + @Test + public void execute_deletes_all_permissions_to_Anyone_on_specified_project_when_new_visibility_is_private() { + ComponentDto project = dbTester.components().insertPublicProject(organization); + UserDto user = dbTester.users().insertUser(); + GroupDto group = dbTester.users().insertGroup(organization); + unsafeGiveAllPermissionsToRootComponent(project, user, group); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PRIVATE) + .execute(); + + verifyHasAllPermissionsButProjectPermissionsToGroupAnyOne(project, user, group); + } + + @Test + public void execute_does_not_delete_all_permissions_to_AnyOne_on_specified_project_if_already_private() { + ComponentDto project = dbTester.components().insertPrivateProject(organization); + UserDto user = dbTester.users().insertUser(); + GroupDto group = dbTester.users().insertGroup(organization); + unsafeGiveAllPermissionsToRootComponent(project, user, group); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PRIVATE) + .execute(); + + verifyStillHasAllPermissions(project, user, group); + } + + @Test + public void execute_deletes_all_permissions_USER_and_BROWSE_of_specified_project_when_new_visibility_is_public() { + ComponentDto project = dbTester.components().insertPrivateProject(organization); + UserDto user = dbTester.users().insertUser(); + GroupDto group = dbTester.users().insertGroup(organization); + unsafeGiveAllPermissionsToRootComponent(project, user, group); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PUBLIC) + .execute(); + + verifyHasAllPermissionsButProjectPermissionsUserAndBrowse(project, user, group); + } + + @Test + public void execute_does_not_delete_permissions_USER_and_BROWSE_of_specified_project_when_new_component_is_already_public() { + ComponentDto project = dbTester.components().insertPublicProject(organization); + UserDto user = dbTester.users().insertUser(); + GroupDto group = dbTester.users().insertGroup(organization); + unsafeGiveAllPermissionsToRootComponent(project, user, group); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PUBLIC) + .execute(); + + verifyStillHasAllPermissions(project, user, group); + } + + @Test + public void execute_does_not_delete_permissions_USER_and_BROWSE_of_specified_view_when_making_it_public() { + ComponentDto view = dbTester.components().insertView(organization); + UserDto user = dbTester.users().insertUser(); + GroupDto group = dbTester.users().insertGroup(organization); + unsafeGiveAllPermissionsToRootComponent(view, user, group); + userSessionRule.addProjectPermission(UserRole.ADMIN, view); + + request.setParam(PARAM_PROJECT, view.key()) + .setParam(PARAM_VISIBILITY, PUBLIC) + .execute(); + + verifyStillHasAllPermissions(view, user, group); + } + + @Test + public void execute_updates_permission_of_specified_project_in_indexes_when_changing_visibility() { + ComponentDto project = randomPublicOrPrivateProject(); + boolean initiallyPrivate = project.isPrivate(); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, initiallyPrivate ? PUBLIC : PRIVATE) + .execute(); + + verify(permissionIndexer).indexProjectsByUuids(any(DbSession.class), eq(Collections.singletonList(project.uuid()))); + } + + @Test + public void execute_does_not_update_permission_of_specified_project_in_indexes_if_already_has_specified_visibility() { + ComponentDto project = randomPublicOrPrivateProject(); + boolean initiallyPrivate = project.isPrivate(); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, initiallyPrivate ? PRIVATE : PUBLIC) + .execute(); + + verifyZeroInteractions(permissionIndexer); + } + + @Test + public void execute_does_not_update_permission_of_specified_view_in_indexes_when_making_it_public() { + ComponentDto view = dbTester.components().insertView(organization); + userSessionRule.addProjectPermission(UserRole.ADMIN, view); + + request.setParam(PARAM_PROJECT, view.key()) + .setParam(PARAM_VISIBILITY, PUBLIC) + .execute(); + + verifyZeroInteractions(permissionIndexer); + } + + @Test + public void execute_grants_USER_and_CODEVIEWER_permissions_to_any_user_with_at_least_one_permission_when_making_project_private() { + ComponentDto project = dbTester.components().insertPublicProject(organization); + UserDto user1 = dbTester.users().insertUser(); + UserDto user2 = dbTester.users().insertUser(); + UserDto user3 = dbTester.users().insertUser(); + dbTester.users().insertProjectPermissionOnUser(user1, "p1", project); + dbTester.users().insertProjectPermissionOnUser(user1, "p2", project); + dbTester.users().insertProjectPermissionOnUser(user2, "p2", project); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PRIVATE) + .execute(); + + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user1.getId(), project.getId())) + .containsOnly(UserRole.USER, UserRole.CODEVIEWER, "p1", "p2"); + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user2.getId(), project.getId())) + .containsOnly(UserRole.USER, UserRole.CODEVIEWER, "p2"); + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user3.getId(), project.getId())) + .isEmpty(); + } + + @Test + public void execute_grants_USER_and_CODEVIEWER_permissions_to_any_group_with_at_least_one_permission_when_making_project_private() { + ComponentDto project = dbTester.components().insertPublicProject(organization); + GroupDto group1 = dbTester.users().insertGroup(organization); + GroupDto group2 = dbTester.users().insertGroup(organization); + GroupDto group3 = dbTester.users().insertGroup(organization); + dbTester.users().insertProjectPermissionOnGroup(group1, "p1", project); + dbTester.users().insertProjectPermissionOnGroup(group1, "p2", project); + dbTester.users().insertProjectPermissionOnGroup(group2, "p2", project); + userSessionRule.addProjectPermission(UserRole.ADMIN, project); + + request.setParam(PARAM_PROJECT, project.key()) + .setParam(PARAM_VISIBILITY, PRIVATE) + .execute(); + + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group1.getId(), project.getId())) + .containsOnly(UserRole.USER, UserRole.CODEVIEWER, "p1", "p2"); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group2.getId(), project.getId())) + .containsOnly(UserRole.USER, UserRole.CODEVIEWER, "p2"); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group3.getId(), project.getId())) + .isEmpty(); + } + + private void unsafeGiveAllPermissionsToRootComponent(ComponentDto component, UserDto user, GroupDto group) { + Arrays.stream(OrganizationPermission.values()) + .forEach(organizationPermission -> { + dbTester.users().insertPermissionOnAnyone(organization, organizationPermission); + dbTester.users().insertPermissionOnGroup(group, organizationPermission); + dbTester.users().insertPermissionOnUser(organization, user, organizationPermission); + }); + ProjectPermissions.ALL + .forEach(permission -> { + unsafeInsertProjectPermissionOnAnyone(component, permission); + unsafeInsertProjectPermissionOnGroup(component, group, permission); + unsafeInsertProjectPermissionOnUser(component, user, permission); + }); + } + + private void unsafeInsertProjectPermissionOnAnyone(ComponentDto component, String permission) { + GroupPermissionDto dto = new GroupPermissionDto() + .setOrganizationUuid(component.getOrganizationUuid()) + .setGroupId(null) + .setRole(permission) + .setResourceId(component.getId()); + dbTester.getDbClient().groupPermissionDao().insert(dbTester.getSession(), dto); + dbTester.commit(); + } + + private void unsafeInsertProjectPermissionOnGroup(ComponentDto component, GroupDto group, String permission) { + GroupPermissionDto dto = new GroupPermissionDto() + .setOrganizationUuid(group.getOrganizationUuid()) + .setGroupId(group.getId()) + .setRole(permission) + .setResourceId(component.getId()); + dbTester.getDbClient().groupPermissionDao().insert(dbTester.getSession(), dto); + dbTester.commit(); + } + + private void unsafeInsertProjectPermissionOnUser(ComponentDto component, UserDto user, String permission) { + UserPermissionDto dto = new UserPermissionDto(component.getOrganizationUuid(), permission, user.getId(), component.getId()); + dbTester.getDbClient().userPermissionDao().insert(dbTester.getSession(), dto); + dbTester.commit(); + } + + private void verifyHasAllPermissionsButProjectPermissionsToGroupAnyOne(ComponentDto component, UserDto user, GroupDto group) { + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), null)) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), group.getId())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), organization.getUuid())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), null, component.getId())) + .isEmpty(); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group.getId(), component.getId())) + .containsAll(ProjectPermissions.ALL); + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user.getId(), component.getId())) + .containsAll(ProjectPermissions.ALL); + } + + private void verifyHasAllPermissionsButProjectPermissionsUserAndBrowse(ComponentDto component, UserDto user, GroupDto group) { + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), null)) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), group.getId())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), organization.getUuid())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), null, component.getId())) + .doesNotContain(UserRole.USER) + .doesNotContain(UserRole.CODEVIEWER) + .containsAll(PROJECT_PERMISSIONS_BUT_USER_AND_CODEVIEWER); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group.getId(), component.getId())) + .doesNotContain(UserRole.USER) + .doesNotContain(UserRole.CODEVIEWER) + .containsAll(PROJECT_PERMISSIONS_BUT_USER_AND_CODEVIEWER); + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user.getId(), component.getId())) + .doesNotContain(UserRole.USER) + .doesNotContain(UserRole.CODEVIEWER) + .containsAll(PROJECT_PERMISSIONS_BUT_USER_AND_CODEVIEWER); + } + + private void verifyStillHasAllPermissions(ComponentDto component, UserDto user, GroupDto group) { + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), null)) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectGlobalPermissionsOfGroup(dbSession, organization.getUuid(), group.getId())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), organization.getUuid())) + .containsAll(ORGANIZATION_PERMISSIONS_NAME_SET); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), null, component.getId())) + .containsAll(ProjectPermissions.ALL); + assertThat(dbClient.groupPermissionDao().selectProjectPermissionsOfGroup(dbSession, organization.getUuid(), group.getId(), component.getId())) + .containsAll(ProjectPermissions.ALL); + assertThat(dbClient.userPermissionDao().selectProjectPermissionsOfUser(dbSession, user.getId(), component.getId())) + .containsAll(ProjectPermissions.ALL); + } + + private void insertPendingTask(ComponentDto project) { + insertCeQueueDto(project, CeQueueDto.Status.PENDING); + } + + private void insertInProgressTask(ComponentDto project) { + insertCeQueueDto(project, CeQueueDto.Status.IN_PROGRESS); + } + + private int counter = 0; + + private void insertCeQueueDto(ComponentDto project, CeQueueDto.Status status) { + dbClient.ceQueueDao().insert(dbTester.getSession(), new CeQueueDto() + .setUuid("pending" + counter++) + .setComponentUuid(project.uuid()) + .setTaskType("foo") + .setStatus(status)); + dbTester.commit(); + } + + private void expectInsufficientPrivilegeException() { + expectedException.expect(ForbiddenException.class); + expectedException.expectMessage("Insufficient privileges"); + } + + private boolean isPrivateInDb(ComponentDto project) { + return dbClient.componentDao().selectByUuid(dbTester.getSession(), project.uuid()).get().isPrivate(); + } + + private ComponentDto randomPublicOrPrivateProject() { + return random.nextBoolean() ? dbTester.components().insertPublicProject(organization) : dbTester.components().insertPrivateProject(organization); + } +} -- 2.39.5