]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9105 add WS api/projects/change_visibility
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 13 Apr 2017 13:44:51 +0000 (15:44 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 27 Apr 2017 12:25:54 +0000 (14:25 +0200)
server/sonar-server/src/main/java/org/sonar/server/project/ws/ProjectsWsModule.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/project/ws/ProjectsWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/project/ws/UpdateVisibilityActionTest.java [new file with mode: 0644]

index 4fcc3bebc44fe10860e3f6d34cc07e889800d2e6..48ce49927e7544e57f6de2a69ee3eab4c797adaa 100644 (file)
@@ -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 (file)
index 0000000..6f37b1f
--- /dev/null
@@ -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<String> 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.<br/>" +
+        "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);
+    });
+  }
+
+}
index 1a8b29a0624a92137f76156da656c9cbfefb8fac..dd594cda0990cdf22804feebaa47833c5ba46223 100644 (file)
@@ -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 (file)
index 0000000..6e2b392
--- /dev/null
@@ -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<String> ORGANIZATION_PERMISSIONS_NAME_SET = stream(OrganizationPermission.values()).map(OrganizationPermission::getKey)
+    .collect(MoreCollectors.toSet(OrganizationPermission.values().length));
+  private static final Set<String> 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);
+  }
+}