]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19965 DELETE /api/v2/users/:login endpoint
authorWojtek Wajerowicz <115081248+wojciech-wajerowicz-sonarsource@users.noreply.github.com>
Tue, 25 Jul 2023 17:58:03 +0000 (19:58 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 26 Jul 2023 20:03:24 +0000 (20:03 +0000)
55 files changed:
server/sonar-webserver-common/build.gradle
server/sonar-webserver-common/src/it/java/org/sonar/server/common/user/service/UserServiceIT.java
server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/ManagedInstanceChecker.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/package-info.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserAnonymizer.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserDeactivator.java [new file with mode: 0644]
server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/service/UserService.java
server/sonar-webserver-common/src/test/java/com/sonar/server/common/management/ManagedInstanceCheckerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java [new file with mode: 0644]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java [deleted file]
server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/config/MockConfigForControllers.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/DefaultUserController.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/user/controller/UserController.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/config/PlatformLevel4WebConfig.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddGroupActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/AddUserActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/RemoveGroupActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/RemoveUserActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/permission/ws/template/ApplyTemplateActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/project/ws/UpdateVisibilityActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/AnonymizeActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/CreateActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DeactivateActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/SearchActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/UpdateActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/UpdateIdentityProviderActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/UpdateLoginActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/UserAnonymizerIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/AddUserActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/CreateActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/RemoveUserActionIT.java
server/sonar-webserver-webapi/src/it/java/org/sonar/server/usergroups/ws/UpdateActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/ManagedInstanceChecker.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/package-info.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddGroupAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/AddUserAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveGroupAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/RemoveUserAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/permission/ws/template/ApplyTemplateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/AnonymizeAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CreateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UpdateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UpdateIdentityProviderAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UpdateLoginAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserAnonymizer.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserDeactivator.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UsersWsModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/CreateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/RemoveUserAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UpdateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/usergroups/ws/UserGroupsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/management/ManagedInstanceCheckerTest.java [deleted file]

index 21daee76c7ac774e2b355f325fbe1d62f45a14c0..4a37f9dfdf62abb88269e1f8ee9e646390045db3 100644 (file)
@@ -9,6 +9,7 @@ dependencies {
     api 'com.google.guava:guava'
 
     api project(':server:sonar-db-dao')
+    api project(':server:sonar-webserver-auth')
     api project(':server:sonar-webserver-ws')
 
     compileOnlyApi 'com.google.code.findbugs:jsr305'
index 769af7dcb7cf35465506d3a2a5e949c164119e88..6b4c5d34d778afca33fa651416cbf9a7c8c4808d 100644 (file)
@@ -35,6 +35,10 @@ import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.common.SearchResults;
 import org.sonar.server.common.avatar.AvatarResolverImpl;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.UserDeactivator;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.management.ManagedInstanceService;
 
 import static java.util.Arrays.asList;
@@ -42,10 +46,15 @@ import static java.util.Collections.singletonList;
 import static java.util.function.Function.identity;
 import static java.util.stream.Collectors.toMap;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 public class UserServiceIT {
@@ -56,7 +65,11 @@ public class UserServiceIT {
 
   private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class);
 
-  private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService);
+  private final ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
+
+  private final UserDeactivator userDeactivator = mock(UserDeactivator.class);
+
+  private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService, managedInstanceChecker, userDeactivator);
 
   @Test
   public void search_for_all_active_users() {
@@ -133,8 +146,7 @@ public class UserServiceIT {
       .extracting(r -> r.userDto().getLogin(), UserSearchResult::managed)
       .containsExactlyInAnyOrder(
         tuple(managedUser.getLogin(), true),
-        tuple(nonManagedUser.getLogin(), false)
-      );
+        tuple(nonManagedUser.getLogin(), false));
 
   }
 
@@ -151,8 +163,7 @@ public class UserServiceIT {
     assertThat(users.searchResults())
       .extracting(r -> r.userDto().getLogin(), UserSearchResult::managed)
       .containsExactlyInAnyOrder(
-        tuple(managedUser.getLogin(), true)
-      );
+        tuple(managedUser.getLogin(), true));
 
   }
 
@@ -169,8 +180,7 @@ public class UserServiceIT {
     assertThat(users.searchResults())
       .extracting(r -> r.userDto().getLogin(), UserSearchResult::managed)
       .containsExactlyInAnyOrder(
-        tuple(nonManagedUser.getLogin(), false)
-      );
+        tuple(nonManagedUser.getLogin(), false));
   }
 
   private void mockInstanceExternallyManagedAndFilterForManagedUsers() {
@@ -243,8 +253,7 @@ public class UserServiceIT {
       .extracting(
         r -> r.userDto().getLogin(),
         userSearchResult -> userSearchResult.userDto().getExternalLogin(),
-        userSearchResult -> userSearchResult.userDto().getExternalIdentityProvider()
-      )
+        userSearchResult -> userSearchResult.userDto().getExternalIdentityProvider())
       .containsExactlyInAnyOrder(tuple(user.getLogin(), user.getExternalLogin(), user.getExternalIdentityProvider()));
   }
 
@@ -371,7 +380,45 @@ public class UserServiceIT {
 
     assertUserWithFilter(b -> b.setSonarLintLastConnectionDateFrom(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), false);
     assertUserWithFilter(b -> b.setSonarLintLastConnectionDateTo(DateUtils.formatDateTime(lastConnection.toEpochMilli())), user.getLogin(), true);
+  }
+
+  @Test
+  public void deactivate_whenUserIsNotFound_shouldThrowNotFoundException() {
+    assertThatThrownBy(() -> userService.deactivate("userToDelete", false))
+      .isInstanceOf(NotFoundException.class)
+      .hasMessage("User 'userToDelete' not found");
+  }
+
+  @Test
+  public void deactivate_whenInstanceIsManagedAndUserIsManaged_shouldThrowBadRequestException() {
+    UserDto user = db.users().insertUser();
+    BadRequestException badRequestException = BadRequestException.create("Not allowed");
+    doThrow(badRequestException).when(managedInstanceChecker).throwIfUserIsManaged(any(), eq(user.getUuid()));
+    assertThatThrownBy(() -> userService.deactivate(user.getLogin(), false))
+      .isEqualTo(badRequestException);
+
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeIsFalse_shouldDeactivateUser() {
+    UserDto user = db.users().insertUser();
+
+    userService.deactivate(user.getLogin(), false);
+    verify(managedInstanceChecker).throwIfUserIsManaged(any(), eq(user.getUuid()));
+
+    verify(userDeactivator).deactivateUser(any(), eq(user.getLogin()));
+    verify(userDeactivator, never()).deactivateUserWithAnonymization(any(), eq(user.getLogin()));
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeIsTrue_shouldDeactivateUserWithAnonymization() {
+    UserDto user = db.users().insertUser();
+
+    userService.deactivate(user.getLogin(), true);
+    verify(managedInstanceChecker).throwIfUserIsManaged(any(), eq(user.getUuid()));
 
+    verify(userDeactivator).deactivateUserWithAnonymization(any(), eq(user.getLogin()));
+    verify(userDeactivator, never()).deactivateUser(any(), eq(user.getLogin()));
   }
 
   private void assertUserWithFilter(Function<UsersSearchRequest.Builder, UsersSearchRequest.Builder> query, String userLogin, boolean isExpectedToBeThere) {
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/ManagedInstanceChecker.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/ManagedInstanceChecker.java
new file mode 100644 (file)
index 0000000..95a24ac
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.common.management;
+
+import org.sonar.db.DbSession;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.management.ManagedProjectService;
+
+public class ManagedInstanceChecker {
+
+  private static final String INSTANCE_EXCEPTION_MESSAGE = "Operation not allowed when the instance is externally managed.";
+  private static final String PROJECT_EXCEPTION_MESSAGE = "Operation not allowed when the project is externally managed.";
+
+  private final ManagedInstanceService managedInstanceService;
+  private final ManagedProjectService managedProjectService;
+
+  public ManagedInstanceChecker(ManagedInstanceService managedInstanceService, ManagedProjectService managedProjectService) {
+    this.managedInstanceService = managedInstanceService;
+    this.managedProjectService = managedProjectService;
+  }
+
+  public void throwIfInstanceIsManaged() {
+    BadRequestException.checkRequest(!managedInstanceService.isInstanceExternallyManaged(), INSTANCE_EXCEPTION_MESSAGE);
+  }
+
+  public void throwIfProjectIsManaged(DbSession dbSession, String projectUuid) {
+    BadRequestException.checkRequest(!managedProjectService.isProjectManaged(dbSession, projectUuid), PROJECT_EXCEPTION_MESSAGE);
+  }
+
+  public void throwIfUserIsManaged(DbSession dbSession, String userUuid) {
+    BadRequestException.checkRequest(!managedInstanceService.isUserManaged(dbSession, userUuid), INSTANCE_EXCEPTION_MESSAGE);
+  }
+
+  public void throwIfUserAndProjectAreManaged(DbSession dbSession, String userUuid, String projectUuid) {
+    boolean isUserManaged = managedInstanceService.isUserManaged(dbSession, userUuid);
+    boolean isProjectManaged = managedProjectService.isProjectManaged(dbSession, projectUuid);
+    BadRequestException.checkRequest(!(isUserManaged && isProjectManaged), PROJECT_EXCEPTION_MESSAGE);
+  }
+
+  public void throwIfGroupAndProjectAreManaged(DbSession dbSession, String groupUuid, String projectUuid) {
+    boolean isGroupManaged = managedInstanceService.isGroupManaged(dbSession, groupUuid);
+    boolean isProjectManaged = managedProjectService.isProjectManaged(dbSession, projectUuid);
+    BadRequestException.checkRequest(!(isGroupManaged && isProjectManaged), PROJECT_EXCEPTION_MESSAGE);
+  }
+
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/package-info.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/management/package-info.java
new file mode 100644 (file)
index 0000000..d9de13f
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.common.management;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserAnonymizer.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserAnonymizer.java
new file mode 100644 (file)
index 0000000..f5094e8
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.common.user;
+
+import java.util.function.Supplier;
+import javax.inject.Inject;
+import org.apache.commons.lang.RandomStringUtils;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ExternalIdentity;
+
+public class UserAnonymizer {
+  private static final int LOGIN_RANDOM_LENGTH = 6;
+
+  private final DbClient dbClient;
+  private final Supplier<String> randomNameGenerator;
+
+  @Inject
+  public UserAnonymizer(DbClient dbClient) {
+    this(dbClient, () -> "sq-removed-" + RandomStringUtils.randomAlphanumeric(LOGIN_RANDOM_LENGTH));
+  }
+
+  public UserAnonymizer(DbClient dbClient, Supplier<String> randomNameGenerator) {
+    this.dbClient = dbClient;
+    this.randomNameGenerator = randomNameGenerator;
+  }
+
+  public void anonymize(DbSession session, UserDto user) {
+    String newLogin = generateAnonymousLogin(session);
+    user
+      .setLogin(newLogin)
+      .setName(newLogin)
+      .setExternalIdentityProvider(ExternalIdentity.SQ_AUTHORITY)
+      .setLocal(true)
+      .setExternalId(newLogin)
+      .setExternalLogin(newLogin);
+  }
+
+  private String generateAnonymousLogin(DbSession session) {
+    for (int i = 0; i < 10; i++) {
+      String candidate = randomNameGenerator.get();
+      if (dbClient.userDao().selectByLogin(session, candidate) == null) {
+        return candidate;
+      }
+    }
+    throw new IllegalStateException("Could not find a unique login");
+  }
+}
diff --git a/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserDeactivator.java b/server/sonar-webserver-common/src/main/java/org/sonar/server/common/user/UserDeactivator.java
new file mode 100644 (file)
index 0000000..9c5fd4d
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.common.user;
+
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyQuery;
+import org.sonar.db.user.UserDto;
+
+import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
+import static org.sonar.server.exceptions.NotFoundException.checkFound;
+
+public class UserDeactivator {
+  private final DbClient dbClient;
+  private final UserAnonymizer userAnonymizer;
+
+  public UserDeactivator(DbClient dbClient, UserAnonymizer userAnonymizer) {
+    this.dbClient = dbClient;
+    this.userAnonymizer = userAnonymizer;
+  }
+
+  public UserDto deactivateUser(DbSession dbSession, String login) {
+    UserDto user = doBeforeDeactivation(dbSession, login);
+    deactivateUser(dbSession, user);
+    return user;
+  }
+
+  public UserDto deactivateUserWithAnonymization(DbSession dbSession, String login) {
+    UserDto user = doBeforeDeactivation(dbSession, login);
+    anonymizeUser(dbSession, user);
+    deactivateUser(dbSession, user);
+    return user;
+  }
+
+  private UserDto doBeforeDeactivation(DbSession dbSession, String login) {
+    UserDto user = getUserOrThrow(dbSession, login);
+    ensureNotLastAdministrator(dbSession, user);
+    deleteRelatedData(dbSession, user);
+    return user;
+  }
+
+  private UserDto getUserOrThrow(DbSession dbSession, String login) {
+    UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
+    return checkFound(user, "User '%s' doesn't exist", login);
+  }
+
+  private void ensureNotLastAdministrator(DbSession dbSession, UserDto user) {
+    boolean isLastAdmin = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingUser(dbSession, ADMINISTER.getKey(), user.getUuid()) == 0;
+    checkRequest(!isLastAdmin, "User is last administrator, and cannot be deactivated");
+  }
+
+  private void deleteRelatedData(DbSession dbSession, UserDto user) {
+    String userUuid = user.getUuid();
+    dbClient.userTokenDao().deleteByUser(dbSession, user);
+    dbClient.propertiesDao().deleteByKeyAndValue(dbSession, DEFAULT_ISSUE_ASSIGNEE, user.getLogin());
+    dbClient.propertiesDao().deleteByQuery(dbSession, PropertyQuery.builder().setUserUuid(userUuid).build());
+    dbClient.userGroupDao().deleteByUserUuid(dbSession, user);
+    dbClient.userPermissionDao().deleteByUserUuid(dbSession, user);
+    dbClient.permissionTemplateDao().deleteUserPermissionsByUserUuid(dbSession, userUuid, user.getLogin());
+    dbClient.qProfileEditUsersDao().deleteByUser(dbSession, user);
+    dbClient.almPatDao().deleteByUser(dbSession, user);
+    dbClient.sessionTokensDao().deleteByUser(dbSession, user);
+    dbClient.userDismissedMessagesDao().deleteByUser(dbSession, user);
+    dbClient.qualityGateUserPermissionDao().deleteByUser(dbSession, user);
+  }
+
+  private void anonymizeUser(DbSession dbSession, UserDto user) {
+    userAnonymizer.anonymize(dbSession, user);
+    dbClient.userDao().update(dbSession, user);
+    dbClient.scimUserDao().deleteByUserUuid(dbSession, user.getUuid());
+  }
+
+  private void deactivateUser(DbSession dbSession, UserDto user) {
+    dbClient.userDao().deactivateUser(dbSession, user);
+    dbSession.commit();
+  }
+}
index 9c5bc4cbe4dcfc04ae2a42546bd81fee10bec1b7..744e41f7df1c9c5d5fd15ca3acfac8b82ccb8dd8 100644 (file)
@@ -32,21 +32,33 @@ import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserQuery;
 import org.sonar.server.common.SearchResults;
 import org.sonar.server.common.avatar.AvatarResolver;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.UserDeactivator;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.management.ManagedInstanceService;
 
 import static java.util.Comparator.comparing;
+import static org.sonar.server.exceptions.NotFoundException.checkFound;
 
 public class UserService {
 
   private final DbClient dbClient;
   private final AvatarResolver avatarResolver;
   private final ManagedInstanceService managedInstanceService;
-
-  public UserService(DbClient dbClient, AvatarResolver avatarResolver, ManagedInstanceService managedInstanceService) {
+  private final ManagedInstanceChecker managedInstanceChecker;
+  private final UserDeactivator userDeactivator;
+
+  public UserService(
+    DbClient dbClient,
+    AvatarResolver avatarResolver,
+    ManagedInstanceService managedInstanceService,
+    ManagedInstanceChecker managedInstanceChecker,
+    UserDeactivator userDeactivator) {
     this.dbClient = dbClient;
     this.avatarResolver = avatarResolver;
     this.managedInstanceService = managedInstanceService;
+    this.managedInstanceChecker = managedInstanceChecker;
+    this.userDeactivator = userDeactivator;
   }
 
   public SearchResults<UserSearchResult> findUsers(UsersSearchRequest request) {
@@ -89,12 +101,11 @@ public class UserService {
     Map<String, Boolean> userUuidToIsManaged = managedInstanceService.getUserUuidToManaged(dbSession, getUserUuids(userDtos));
     return userDtos.stream()
       .map(userDto -> toUserSearchResult(
-          groupsByLogin.get(userDto.getLogin()),
-          tokenCountsByLogin.getOrDefault(userDto.getUuid(), 0),
-          userUuidToIsManaged.getOrDefault(userDto.getUuid(), false),
-          userDto
-        )
-      ).toList();
+        groupsByLogin.get(userDto.getLogin()),
+        tokenCountsByLogin.getOrDefault(userDto.getUuid(), 0),
+        userUuidToIsManaged.getOrDefault(userDto.getUuid(), false),
+        userDto))
+      .toList();
   }
 
   private UserSearchResult toUserSearchResult(Collection<String> groups, int tokenCount, boolean managed, UserDto userDto) {
@@ -103,8 +114,7 @@ public class UserService {
       managed,
       findAvatar(userDto),
       groups,
-      tokenCount
-    );
+      tokenCount);
   }
 
   private List<UserDto> findUsersAndSortByLogin(DbSession dbSession, UserQuery userQuery, int page, int pageSize) {
@@ -122,4 +132,18 @@ public class UserService {
     return users.stream().map(UserDto::getUuid).collect(Collectors.toSet());
   }
 
+  public UserDto deactivate(String login, Boolean anonymize) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      UserDto userDto = checkFound(dbClient.userDao().selectByLogin(dbSession, login), "User '%s' not found", login);
+      managedInstanceChecker.throwIfUserIsManaged(dbSession, userDto.getUuid());
+      UserDto deactivatedUser;
+      if (Boolean.TRUE.equals(anonymize)) {
+        deactivatedUser = userDeactivator.deactivateUserWithAnonymization(dbSession, login);
+      } else {
+        deactivatedUser = userDeactivator.deactivateUser(dbSession, login);
+      }
+      dbSession.commit();
+      return deactivatedUser;
+    }
+  }
 }
diff --git a/server/sonar-webserver-common/src/test/java/com/sonar/server/common/management/ManagedInstanceCheckerTest.java b/server/sonar-webserver-common/src/test/java/com/sonar/server/common/management/ManagedInstanceCheckerTest.java
new file mode 100644 (file)
index 0000000..4efc912
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 com.sonar.server.common.management;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.sonar.db.DbSession;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.db.user.GroupDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.management.ManagedInstanceService;
+import org.sonar.server.management.ManagedProjectService;
+
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ManagedInstanceCheckerTest {
+
+  private static final String INSTANCE_EXCEPTION_MESSAGE = "Operation not allowed when the instance is externally managed.";
+  private static final String PROJECT_EXCEPTION_MESSAGE = "Operation not allowed when the project is externally managed.";
+
+  @Mock
+  private DbSession dbSession;
+  @Mock
+  private ManagedInstanceService managedInstanceService;
+  @Mock
+  private ManagedProjectService managedProjectService;
+  @InjectMocks
+  private ManagedInstanceChecker managedInstanceChecker;
+
+  @Test
+  public void throwIfInstanceIsManaged_whenInstanceExternallyManaged_shouldThrow() {
+    when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
+
+    assertThatThrownBy(() -> managedInstanceChecker.throwIfInstanceIsManaged())
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage(INSTANCE_EXCEPTION_MESSAGE);
+  }
+
+  @Test
+  public void throwIfInstanceIsManaged_whenInstanceNotExternallyManaged_shouldNotThrow() {
+    when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(false);
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfInstanceIsManaged());
+  }
+
+  @Test
+  public void throwIfProjectIsManaged_whenProjectIsManaged_shouldThrow() {
+    ProjectDto projectDto = mockManagedProject();
+
+    String projectUuid = projectDto.getUuid();
+    assertThatThrownBy(() -> managedInstanceChecker.throwIfProjectIsManaged(dbSession, projectUuid))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
+  }
+
+  @Test
+  public void throwIfProjectIsManaged_whenProjectIsNotManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockNotManagedProject();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfProjectIsManaged(dbSession, projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfUserIsManaged_whenUserIsManaged_shouldThrow() {
+    UserDto userDto = mockManagedUser();
+
+    String userUuid = userDto.getUuid();
+    assertThatThrownBy(() -> managedInstanceChecker.throwIfUserIsManaged(dbSession, userUuid))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage(INSTANCE_EXCEPTION_MESSAGE);
+  }
+
+  @Test
+  public void throwIfUserIsManaged_whenUserIsNotManaged_shouldNotThrow() {
+    UserDto userDto = mockNotManagedUser();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserIsManaged(dbSession, userDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfUserAndProjectAreManaged_whenUserAndProjectAreManaged_shouldThrow() {
+    ProjectDto projectDto = mockManagedProject();
+    UserDto userDto = mockManagedUser();
+
+    String userUuid = userDto.getUuid();
+    String projectUuid = projectDto.getUuid();
+    assertThatThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userUuid, projectUuid))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
+  }
+
+  @Test
+  public void throwIfUserAndProjectAreManaged_whenOnlyUserIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockNotManagedProject();
+    UserDto userDto = mockManagedUser();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfUserAndProjectAreManaged_whenOnlyProjectIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockManagedProject();
+    UserDto userDto = mockNotManagedUser();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfUserAndProjectAreManaged_whenNothingIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockNotManagedProject();
+    UserDto userDto = mockNotManagedUser();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfGroupAndProjectAreManaged_whenGroupAndProjectAreManaged_shouldThrow() {
+    ProjectDto projectDto = mockManagedProject();
+    GroupDto groupDto = mockManagedGroup();
+
+    String groupDtoUuid = groupDto.getUuid();
+    String projectDtoUuid = projectDto.getUuid();
+    assertThatThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDtoUuid, projectDtoUuid))
+      .isInstanceOf(BadRequestException.class)
+      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
+  }
+
+  @Test
+  public void throwIfGroupAndProjectAreManaged_whenOnlyGroupIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockNotManagedProject();
+    GroupDto groupDto = mockManagedGroup();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfGroupAndProjectAreManaged_whenOnlyProjectIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockManagedProject();
+    GroupDto groupDto = mockNotManagedGroup();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
+  }
+
+  @Test
+  public void throwIfGroupAndProjectAreManaged_whenNothingIsManaged_shouldNotThrow() {
+    ProjectDto projectDto = mockNotManagedProject();
+    GroupDto groupDto = mockNotManagedGroup();
+
+    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
+  }
+
+  private ProjectDto mockManagedProject() {
+    return mockProject(true);
+  }
+
+  private ProjectDto mockNotManagedProject() {
+    return mockProject(false);
+  }
+
+  private ProjectDto mockProject(boolean isManaged) {
+    ProjectDto projectDto = mock(ProjectDto.class);
+    when(managedProjectService.isProjectManaged(dbSession, projectDto.getUuid())).thenReturn(isManaged);
+    return projectDto;
+  }
+
+  private UserDto mockManagedUser() {
+    return mockUser(true);
+  }
+
+  private UserDto mockNotManagedUser() {
+    return mockUser(false);
+  }
+
+  private UserDto mockUser(boolean isManaged) {
+    UserDto userDto = mock(UserDto.class);
+    when(managedInstanceService.isUserManaged(dbSession, userDto.getUuid())).thenReturn(isManaged);
+    return userDto;
+  }
+
+  private GroupDto mockManagedGroup() {
+    return mockGroup(true);
+  }
+
+  private GroupDto mockNotManagedGroup() {
+    return mockGroup(false);
+  }
+
+  private GroupDto mockGroup(boolean isManaged) {
+    GroupDto groupDto = mock(GroupDto.class);
+    when(managedInstanceService.isGroupManaged(dbSession, groupDto.getUuid())).thenReturn(isManaged);
+    return groupDto;
+  }
+
+}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerIT.java
new file mode 100644 (file)
index 0000000..5298c35
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.v2.api.user.controller;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.db.DbClient;
+import org.sonar.db.user.UserDao;
+import org.sonar.server.common.user.service.UserService;
+import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.user.AbstractUserSession;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.v2.common.ControllerIT;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.v2.WebApiEndpoints.USER_ENDPOINT;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class DefaultUserControllerIT extends ControllerIT {
+
+  @After
+  public void resetUsedMocks() {
+    Mockito.reset(webAppContext.getBean(UserService.class));
+    Mockito.reset(webAppContext.getBean(UserSession.class));
+  }
+
+  @Before
+  public void setUp() {
+    UserSession userSession = webAppContext.getBean(UserSession.class);
+    when(userSession.checkLoggedIn()).thenReturn(userSession);
+  }
+
+  @Test
+  public void deactivate_whenUserIsNotLoggedIn_shouldReturnForbidden() throws Exception {
+    when(webAppContext.getBean(UserSession.class).checkLoggedIn()).thenThrow(new UnauthorizedException("unauthorized"));
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isUnauthorized(),
+        content().string("{\"message\":\"unauthorized\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserIsNotAdministrator_shouldReturnForbidden() throws Exception {
+    when(webAppContext.getBean(UserSession.class).checkIsSystemAdministrator()).thenThrow(AbstractUserSession.insufficientPrivilegesException());
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isForbidden(),
+        content().json("{\"message\":\"Insufficient privileges\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserServiceThrowsNotFoundException_shouldReturnNotFound() throws Exception {
+    doThrow(new NotFoundException("User not found.")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isNotFound(),
+        content().json("{\"message\":\"User not found.\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserServiceThrowsBadRequestException_shouldReturnBadRequest() throws Exception {
+    doThrow(BadRequestException.create("Not allowed")).when(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json("{\"message\":\"Not allowed\"}"));
+  }
+
+  @Test
+  public void deactivate_whenUserTryingToDeactivateThemself_shouldReturnBadRequest() throws Exception {
+    when(webAppContext.getBean(DbClient.class).userDao()).thenReturn(mock(UserDao.class));
+    when(webAppContext.getBean(UserSession.class).getLogin()).thenReturn("userToDelete");
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpectAll(
+        status().isBadRequest(),
+        content().json("{\"message\":\"Self-deactivation is not possible\"}"));
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeParameterIsNotBoolean_shouldReturnBadRequest() throws Exception {
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=maybe"))
+      .andExpect(
+        status().isBadRequest());
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeIsNotSpecified_shouldDeactivateUserWithoutAnonymization() throws Exception {
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete"))
+      .andExpect(status().isNoContent());
+
+    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeFalse_shouldDeactivateUserWithoutAnonymization() throws Exception {
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=false"))
+      .andExpect(status().isNoContent());
+
+    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", false);
+  }
+
+  @Test
+  public void deactivate_whenAnonymizeTrue_shouldDeactivateUserWithAnonymization() throws Exception {
+    mockMvc.perform(delete(USER_ENDPOINT + "/userToDelete?anonymize=true"))
+      .andExpect(status().isNoContent());
+
+    verify(webAppContext.getBean(UserService.class)).deactivate("userToDelete", true);
+  }
+}
diff --git a/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java b/server/sonar-webserver-webapi-v2/src/it/java/org/sonar/server/v2/api/user/controller/DefaultUserControllerTest.java
deleted file mode 100644 (file)
index 9073d3b..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.v2.api.user.controller;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class DefaultUserControllerTest {
-
-}
index 12bfee0eb87c5cac3fd79fda3cb806f7b5c96290..cee83db83499d3ace4a5f165bc729ba2ddcfa4e5 100644 (file)
@@ -26,7 +26,6 @@ import org.sonar.server.health.DbConnectionNodeCheck;
 import org.sonar.server.health.EsStatusNodeCheck;
 import org.sonar.server.health.HealthChecker;
 import org.sonar.server.health.WebServerStatusNodeCheck;
-import org.sonar.server.management.ManagedInstanceChecker;
 import org.sonar.server.platform.NodeInformation;
 import org.sonar.server.platform.ws.LivenessChecker;
 import org.sonar.server.user.SystemPasscode;
@@ -95,13 +94,9 @@ public class MockConfigForControllers {
     return mock(UserUpdater.class);
   }
 
-  @Bean
-  ManagedInstanceChecker managedInstanceChecker() {
-    return mock(ManagedInstanceChecker.class);
-  }
-
   @Bean
   UserService userService() {
     return mock(UserService.class);
   }
+
 }
index c095a720cd1faf85e2e761e4047c03769caeebbd..4aa6fed9ed053ff221e1222e0f6e5b239f651389 100644 (file)
@@ -34,13 +34,17 @@ import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
 import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
 
 import static org.sonar.api.utils.Paging.forPageIndex;
+import static org.sonar.server.exceptions.BadRequestException.checkRequest;
 
 public class DefaultUserController implements UserController {
   private final UsersSearchRestResponseGenerator usersSearchResponseGenerator;
   private final UserService userService;
   private final UserSession userSession;
 
-  public DefaultUserController(UserSession userSession, UserService userService, UsersSearchRestResponseGenerator usersSearchResponseGenerator) {
+  public DefaultUserController(
+    UserSession userSession,
+    UserService userService,
+    UsersSearchRestResponseGenerator usersSearchResponseGenerator) {
     this.userSession = userSession;
     this.usersSearchResponseGenerator = usersSearchResponseGenerator;
     this.userService = userService;
@@ -87,4 +91,10 @@ public class DefaultUserController implements UserController {
       .build();
   }
 
+  @Override
+  public void deactivate(String login, Boolean anonymize) {
+    userSession.checkLoggedIn().checkIsSystemAdministrator();
+    checkRequest(!login.equals(userSession.getLogin()), "Self-deactivation is not possible");
+    userService.deactivate(login, anonymize);
+  }
 }
index 03181b3438a28471d3213a386d8be9910a41b7ec..fc4e35c4ebd05366296d11a67893a534ceb60c69 100644 (file)
@@ -20,6 +20,8 @@
 package org.sonar.server.v2.api.user.controller;
 
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
 import javax.validation.Valid;
 import org.sonar.server.v2.api.model.RestPage;
 import org.sonar.server.v2.api.user.request.UsersSearchRestRequest;
@@ -27,8 +29,11 @@ import org.sonar.server.v2.api.user.response.UsersSearchRestResponse;
 import org.springdoc.api.annotations.ParameterObject;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseStatus;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -53,4 +58,11 @@ public interface UserController {
       Field 'sonarqubeLastConnectionDate' is only updated every hour, so it may not be accurate, for instance when a user authenticates many times in less than one hour.
     """)
   UsersSearchRestResponse search(@ParameterObject UsersSearchRestRequest usersSearchRestRequest, @Valid @ParameterObject RestPage restPage);
+
+  @DeleteMapping(path = "/{login}")
+  @ResponseStatus(HttpStatus.NO_CONTENT)
+  @Operation(summary = "Deactivate a user", description = "Deactivate a user. Requires Administer System permission.")
+  void deactivate(
+    @PathVariable("login") @Parameter(description = "The login of the user to delete.", required = true, in = ParameterIn.PATH) String login,
+    @RequestParam(value = "anonymize", required = false, defaultValue = "false") @Parameter(description = "Anonymize user in addition to deactivating it.") Boolean anonymize);
 }
index d6c133308df0b99cec66782f2ae61d81a870d8bf..fc29d481951e3c4f216592a874d971767660d1ba 100644 (file)
@@ -68,7 +68,10 @@ public class PlatformLevel4WebConfig {
   }
 
   @Bean
-  public UserController userController(UserSession userSession, UsersSearchRestResponseGenerator usersSearchResponseGenerator, UserService userService) {
+  public UserController userController(
+    UserSession userSession,
+    UsersSearchRestResponseGenerator usersSearchResponseGenerator,
+    UserService userService) {
     return new DefaultUserController(userSession, userService, usersSearchResponseGenerator);
   }
 
index 43de8249cda49a033cc9a16f1cb5ca669a933992..2aa463a7cd685615c6399e087ab741e99a07b4e2 100644 (file)
@@ -36,7 +36,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionServiceImpl;
 import org.sonar.server.ws.TestRequest;
index 6e8cb3c838f8261cba8d2017f09d9dae80e0116e..50856df0e559e52ae4df86d3d4886663593c9e2a 100644 (file)
@@ -35,7 +35,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionServiceImpl;
 import org.sonar.server.ws.TestRequest;
index b5e85e7be69ccef851a6b65981463f539033753e..9a550e33db258bad8fe034cfab6d3b4c895fc8f9 100644 (file)
@@ -40,7 +40,7 @@ import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionServiceImpl;
 import org.sonar.server.ws.TestRequest;
index 8af083dccb3f84a9438ab0631be2885ad525d7b8..9f6744f0a0c1c66171e6f3f7067bb697888ffcf3 100644 (file)
@@ -33,7 +33,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionServiceImpl;
 import org.sonar.server.ws.TestRequest;
index 594d47573bfc6deb38958e211b54f095d5753c45..69c3ae7db577d1aedb619773497e61e1b1a4d743 100644 (file)
@@ -38,7 +38,7 @@ import org.sonar.server.es.TestIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.management.ManagedProjectService;
 import org.sonar.server.permission.DefaultTemplatesResolver;
 import org.sonar.server.permission.DefaultTemplatesResolverImpl;
index 6f68d7cba98c34b1995e9f4f814288ab6422300b..48c186e64564524baa85e7437c4267d4736e64c6 100644 (file)
@@ -59,7 +59,7 @@ import org.sonar.server.es.TestIndexers;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionServiceImpl;
 import org.sonar.server.permission.index.FooIndexDefinition;
index a03f4399b57b594409509894bd545ee06000455e..85bebb1673f5a919945cab032a9c273c5b46c3c2 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserQuery;
+import org.sonar.server.common.user.UserAnonymizer;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
index 5af7f5a8c5ef88a86d8c412eb14fd44b4916ffce..5071be725cdd224b37f7688759ba3df3c6d6b463 100644 (file)
@@ -36,7 +36,7 @@ import org.sonar.server.authentication.CredentialsLocalAuthentication;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
index 2afc43c82d43fa7ababb1629ed8e4449cb30c3ca..129f5561d382a1e780d45e691f002052d85a2e85 100644 (file)
@@ -44,11 +44,16 @@ import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.SessionTokenDto;
 import org.sonar.db.user.UserDismissedMessageDto;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.common.avatar.AvatarResolver;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.UserAnonymizer;
+import org.sonar.server.common.user.UserDeactivator;
+import org.sonar.server.common.user.service.UserService;
 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.management.ManagedInstanceChecker;
+import org.sonar.server.management.ManagedInstanceService;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.ExternalIdentity;
 import org.sonar.server.ws.TestRequest;
@@ -80,7 +85,8 @@ public class DeactivateActionIT {
   private final UserDeactivator userDeactivator = new UserDeactivator(dbClient, userAnonymizer);
   private final ManagedInstanceChecker managedInstanceChecker = mock(ManagedInstanceChecker.class);
 
-  private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userSession, new UserJsonWriter(userSession), userDeactivator, managedInstanceChecker));
+  private final UserService userService = new UserService(dbClient, mock(AvatarResolver.class), mock(ManagedInstanceService.class), managedInstanceChecker, userDeactivator);
+  private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userSession, new UserJsonWriter(userSession), userService));
 
   @Test
   public void deactivate_user_and_delete_their_related_data() {
@@ -342,7 +348,7 @@ public class DeactivateActionIT {
       deactivate("someone");
     })
       .isInstanceOf(NotFoundException.class)
-      .hasMessage("User 'someone' doesn't exist");
+      .hasMessage("User 'someone' not found");
   }
 
   @Test
index f75975c1c66bdffc90a9fad71622224f4f5ca047..732d219d5a0aea9e2c2576b89119c997aae9ace7 100644 (file)
@@ -37,6 +37,8 @@ import org.sonar.db.scim.ScimUserDao;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.common.avatar.AvatarResolverImpl;
+import org.sonar.server.common.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.UserDeactivator;
 import org.sonar.server.common.user.service.UserService;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ServerException;
@@ -74,7 +76,12 @@ public class SearchActionIT {
 
   private final ManagedInstanceService managedInstanceService = mock(ManagedInstanceService.class);
 
-  private final UserService userService = new UserService(db.getDbClient(), new AvatarResolverImpl(), managedInstanceService);
+  private final UserService userService = new UserService(
+    db.getDbClient(),
+    new AvatarResolverImpl(),
+    managedInstanceService,
+    mock(ManagedInstanceChecker.class),
+    mock(UserDeactivator.class));
 
   private final SearchWsReponseGenerator searchWsReponseGenerator = new SearchWsReponseGenerator(userSession);
 
@@ -168,8 +175,7 @@ public class SearchActionIT {
       .extracting(User::getLogin, User::getManaged)
       .containsExactlyInAnyOrder(
         tuple(managedUser.getLogin(), true),
-        tuple(nonManagedUser.getLogin(), false)
-      );
+        tuple(nonManagedUser.getLogin(), false));
   }
 
   @Test
@@ -200,8 +206,7 @@ public class SearchActionIT {
     assertThat(response.getUsersList())
       .extracting(User::getLogin, User::getManaged)
       .containsExactlyInAnyOrder(
-        tuple(managedUser.getLogin(), true)
-      );
+        tuple(managedUser.getLogin(), true));
   }
 
   @Test
@@ -220,8 +225,7 @@ public class SearchActionIT {
     assertThat(response.getUsersList())
       .extracting(User::getLogin, User::getManaged)
       .containsExactlyInAnyOrder(
-        tuple(nonManagedUser.getLogin(), false)
-      );
+        tuple(nonManagedUser.getLogin(), false));
   }
 
   private void mockInstanceExternallyManagedAndFilterForManagedUsers() {
@@ -558,7 +562,7 @@ public class SearchActionIT {
     userSession.logIn();
 
     Stream.of(SearchAction.LAST_CONNECTION_DATE_FROM, SearchAction.LAST_CONNECTION_DATE_TO,
-        SearchAction.SONAR_LINT_LAST_CONNECTION_DATE_FROM, SearchAction.SONAR_LINT_LAST_CONNECTION_DATE_TO)
+      SearchAction.SONAR_LINT_LAST_CONNECTION_DATE_FROM, SearchAction.SONAR_LINT_LAST_CONNECTION_DATE_TO)
       .map(param -> ws.newRequest().setParam(param, formatDateTime(OffsetDateTime.now())))
       .forEach(SearchActionIT::assertForbiddenException);
   }
index cf0f8bfee69a370b633d74844a7326dc04fa4556..78a7a1097d51389581a1ed14f3d654694a0dd871 100644 (file)
@@ -36,7 +36,7 @@ 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.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
index 125339b82ba602e830467f37e1cadbbb6d622934..1149597c0fa4011c8697e82a635de9dc01788d78 100644 (file)
@@ -33,7 +33,7 @@ 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.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
index ef372d567d0ff6602314da5ae15777ceabe30f0f..9853b0b3979219a1d36d681b69cb2ee3f97297c3 100644 (file)
@@ -30,7 +30,7 @@ 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.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.NewUserNotifier;
 import org.sonar.server.user.UserUpdater;
index e10daea53a458533a36c91b47bbf87a4c9f1f08c..8097985ce568c7eb710228b049b208fbfafce1cd 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.common.user.UserAnonymizer;
 import org.sonar.server.user.ExternalIdentity;
 
 import static org.assertj.core.api.Assertions.assertThat;
index eae0bc78ecb710c3d4dc2c3bef3e602a7a6884e0..99f386075fa56fa53fd9de82d2b27c46cad55ab7 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 import org.sonar.server.ws.TestRequest;
index 42043b7af36c0118dab342158540695f3cb6ccf9..3541a23cfb00cd3497b1d78a83d42e73058bcda4 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.db.user.GroupDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
index 088f91aef2d6031ef351752e537512e80d27f790..69a3c6913dbe8c6f4b12443c7ee366b1dcba2951 100644 (file)
@@ -30,7 +30,7 @@ import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.usergroups.DefaultGroupFinder;
 import org.sonar.server.ws.TestRequest;
index c065b8e5300fe5b9e68d97dc92a0bbf47ea409c5..787af00f823da77464a200380370dbf858d8dd34 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.ServerException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.TestRequest;
 import org.sonar.server.ws.WsActionTester;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/ManagedInstanceChecker.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/ManagedInstanceChecker.java
deleted file mode 100644 (file)
index b5183f5..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.management;
-
-import org.sonar.db.DbSession;
-import org.sonar.server.exceptions.BadRequestException;
-
-public class ManagedInstanceChecker {
-
-  private static final String INSTANCE_EXCEPTION_MESSAGE = "Operation not allowed when the instance is externally managed.";
-  private static final String PROJECT_EXCEPTION_MESSAGE = "Operation not allowed when the project is externally managed.";
-
-  private final ManagedInstanceService managedInstanceService;
-  private final ManagedProjectService managedProjectService;
-
-  public ManagedInstanceChecker(ManagedInstanceService managedInstanceService, ManagedProjectService managedProjectService) {
-    this.managedInstanceService = managedInstanceService;
-    this.managedProjectService = managedProjectService;
-  }
-
-  public void throwIfInstanceIsManaged() {
-    BadRequestException.checkRequest(!managedInstanceService.isInstanceExternallyManaged(), INSTANCE_EXCEPTION_MESSAGE);
-  }
-
-  public void throwIfProjectIsManaged(DbSession dbSession, String projectUuid) {
-    BadRequestException.checkRequest(!managedProjectService.isProjectManaged(dbSession, projectUuid), PROJECT_EXCEPTION_MESSAGE);
-  }
-
-  public void throwIfUserIsManaged(DbSession dbSession, String userUuid) {
-    BadRequestException.checkRequest(!managedInstanceService.isUserManaged(dbSession, userUuid), INSTANCE_EXCEPTION_MESSAGE);
-  }
-
-  public void throwIfUserAndProjectAreManaged(DbSession dbSession, String userUuid, String projectUuid) {
-    boolean isUserManaged = managedInstanceService.isUserManaged(dbSession, userUuid);
-    boolean isProjectManaged = managedProjectService.isProjectManaged(dbSession, projectUuid);
-    BadRequestException.checkRequest(!(isUserManaged && isProjectManaged), PROJECT_EXCEPTION_MESSAGE);
-  }
-
-  public void throwIfGroupAndProjectAreManaged(DbSession dbSession, String groupUuid, String projectUuid) {
-    boolean isGroupManaged = managedInstanceService.isGroupManaged(dbSession, groupUuid);
-    boolean isProjectManaged = managedProjectService.isProjectManaged(dbSession, projectUuid);
-    BadRequestException.checkRequest(!(isGroupManaged && isProjectManaged), PROJECT_EXCEPTION_MESSAGE);
-  }
-
-}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/package-info.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/management/package-info.java
deleted file mode 100644 (file)
index 22b8574..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.server.management;
-
-import javax.annotation.ParametersAreNonnullByDefault;
index eb71e9b57991d117429b977fce68acc416731501..f0265f8a32a998c7eef5e385b9b3d708da378bdd 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.db.user.GroupDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.GroupPermissionChange;
 import org.sonar.server.permission.PermissionChange;
 import org.sonar.server.permission.PermissionService;
index f8a013befc86056dafbca01a694ca1287322bf3b..e119759bb022d24d36f3e37b0b3056e0848455ac 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.db.user.UserId;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionChange;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionUpdater;
index 39c38f84c39c0a42de8c6abcc1ee333effee2105..60d429f55eb1526090c3d52e8ca11ab902e7a961 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.db.user.GroupDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.GroupPermissionChange;
 import org.sonar.server.permission.GroupUuidOrAnyone;
 import org.sonar.server.permission.PermissionChange;
index 17f8b875b9405fc721e42ae6c59551e8059019a9..d688ad8d78a604b98958a4ddab691e91f556ef80 100644 (file)
@@ -26,7 +26,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.db.user.UserId;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionChange;
 import org.sonar.server.permission.PermissionService;
 import org.sonar.server.permission.PermissionUpdater;
index 0e90558f9dc6990f90ed77c94fb345220a4db77a..41cdd943605a503006258225c7376013db7a89af 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.db.permission.template.PermissionTemplateDto;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.permission.PermissionTemplateService;
 import org.sonar.server.permission.ws.PermissionWsSupport;
 import org.sonar.server.permission.ws.PermissionsWsAction;
index 5148c6b6ea5e9e3ffc78103815a6df95584e5553..cb5f87b72302a0cafd2f1d56dbae79c7f5714414 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.entity.EntityDto;
 import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.project.Visibility;
 import org.sonar.server.project.VisibilityService;
 import org.sonar.server.user.UserSession;
index d26ffd9eeeb87c7b183c86a83f66e255c7cb1475..a79942645b5e432ccf153a208002c6d58ee200f5 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.server.ws.WebService.NewAction;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
+import org.sonar.server.common.user.UserAnonymizer;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.server.exceptions.NotFoundException.checkFound;
index c7c11e916aed85fa2a5d2f2b8c9aa7abee20ebfa..839e26979189a7e43b8d252a5a4c16ae6efd2569 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.ExternalIdentity;
 import org.sonar.server.user.NewUser;
 import org.sonar.server.user.UserSession;
index 3dfa62dab2b0e4efe33d798c0fb74b9ee68be7ae..b154e0cece80e0b623de84824d2c4633d8176080 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.user.service.UserService;
 import org.sonar.server.user.UserSession;
 
 import static java.util.Collections.singletonList;
@@ -44,16 +44,13 @@ public class DeactivateAction implements UsersWsAction {
   private final DbClient dbClient;
   private final UserSession userSession;
   private final UserJsonWriter userWriter;
-  private final UserDeactivator userDeactivator;
-  private final ManagedInstanceChecker managedInstanceChecker;
+  private final UserService userService;
 
-  public DeactivateAction(DbClient dbClient, UserSession userSession, UserJsonWriter userWriter,
-    UserDeactivator userDeactivator, ManagedInstanceChecker managedInstanceChecker) {
+  public DeactivateAction(DbClient dbClient, UserSession userSession, UserJsonWriter userWriter, UserService userService) {
     this.dbClient = dbClient;
     this.userSession = userSession;
     this.userWriter = userWriter;
-    this.userDeactivator = userDeactivator;
-    this.managedInstanceChecker = managedInstanceChecker;
+    this.userService = userService;
   }
 
   @Override
@@ -83,17 +80,9 @@ public class DeactivateAction implements UsersWsAction {
     userSession.checkLoggedIn().checkIsSystemAdministrator();
     String login = request.mandatoryParam(PARAM_LOGIN);
     checkRequest(!login.equals(userSession.getLogin()), "Self-deactivation is not possible");
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      UserDto userDto = dbClient.userDao().selectByLogin(dbSession, login);
-      if (userDto != null) {
-        managedInstanceChecker.throwIfUserIsManaged(dbSession, userDto.getUuid());
-      }
-      boolean shouldAnonymize = request.mandatoryParamAsBoolean(PARAM_ANONYMIZE);
-      userDto = shouldAnonymize
-        ? userDeactivator.deactivateUserWithAnonymization(dbSession, login)
-        : userDeactivator.deactivateUser(dbSession, login);
-      writeResponse(response, userDto.getLogin());
-    }
+    boolean shouldAnonymize = request.mandatoryParamAsBoolean(PARAM_ANONYMIZE);
+    UserDto deactivatedUser = userService.deactivate(login, shouldAnonymize);
+    writeResponse(response, deactivatedUser.getLogin());
   }
 
   private void writeResponse(Response response, String login) {
@@ -113,6 +102,4 @@ public class DeactivateAction implements UsersWsAction {
     }
   }
 
-
-
 }
index 73af1722dc647dd26af22df72c1ca19143799009..08cba7568405efcdfe0f4a0c96e49be35e3d2961 100644 (file)
@@ -34,7 +34,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UpdateUser;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.user.UserUpdater;
index e4d4f89e95da3267345c7f96aae2b3966d5176ec..d2b9404c8e11261f463b413a1ae92d51592ebad6 100644 (file)
@@ -32,7 +32,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.authentication.IdentityProviderRepository;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.ExternalIdentity;
 import org.sonar.server.user.UpdateUser;
 import org.sonar.server.user.UserSession;
index 570cdc8c4cd50bcdb08c7c77046583b165a9822d..ac4bee4d28f84b123da21f412a52ad7d4fd62001 100644 (file)
@@ -26,7 +26,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UpdateUser;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.user.UserUpdater;
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserAnonymizer.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserAnonymizer.java
deleted file mode 100644 (file)
index cd406ef..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.user.ws;
-
-import java.util.function.Supplier;
-import javax.inject.Inject;
-import org.apache.commons.lang.RandomStringUtils;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.user.ExternalIdentity;
-
-public class UserAnonymizer {
-  private static final int LOGIN_RANDOM_LENGTH = 6;
-
-  private final DbClient dbClient;
-  private final Supplier<String> randomNameGenerator;
-
-  @Inject
-  public UserAnonymizer(DbClient dbClient) {
-    this(dbClient, () -> "sq-removed-" + RandomStringUtils.randomAlphanumeric(LOGIN_RANDOM_LENGTH));
-  }
-
-  public UserAnonymizer(DbClient dbClient, Supplier<String> randomNameGenerator) {
-    this.dbClient = dbClient;
-    this.randomNameGenerator = randomNameGenerator;
-  }
-
-  public void anonymize(DbSession session, UserDto user) {
-    String newLogin = generateAnonymousLogin(session);
-    user
-      .setLogin(newLogin)
-      .setName(newLogin)
-      .setExternalIdentityProvider(ExternalIdentity.SQ_AUTHORITY)
-      .setLocal(true)
-      .setExternalId(newLogin)
-      .setExternalLogin(newLogin);
-  }
-
-  private String generateAnonymousLogin(DbSession session) {
-    for (int i = 0; i < 10; i++) {
-      String candidate = randomNameGenerator.get();
-      if (dbClient.userDao().selectByLogin(session, candidate) == null) {
-        return candidate;
-      }
-    }
-    throw new IllegalStateException("Could not find a unique login");
-  }
-}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserDeactivator.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserDeactivator.java
deleted file mode 100644 (file)
index 127a8ba..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.user.ws;
-
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.property.PropertyQuery;
-import org.sonar.db.user.UserDto;
-
-import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
-import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
-import static org.sonar.server.exceptions.BadRequestException.checkRequest;
-import static org.sonar.server.exceptions.NotFoundException.checkFound;
-
-public class UserDeactivator {
-  private final DbClient dbClient;
-  private final UserAnonymizer userAnonymizer;
-
-  public UserDeactivator(DbClient dbClient, UserAnonymizer userAnonymizer) {
-    this.dbClient = dbClient;
-    this.userAnonymizer = userAnonymizer;
-  }
-
-  public UserDto deactivateUser(DbSession dbSession, String login) {
-    UserDto user = doBeforeDeactivation(dbSession, login);
-    deactivateUser(dbSession, user);
-    return user;
-  }
-
-  public UserDto deactivateUserWithAnonymization(DbSession dbSession, String login) {
-    UserDto user = doBeforeDeactivation(dbSession, login);
-    anonymizeUser(dbSession, user);
-    deactivateUser(dbSession, user);
-    return user;
-  }
-
-  private UserDto doBeforeDeactivation(DbSession dbSession, String login) {
-    UserDto user = getUserOrThrow(dbSession, login);
-    ensureNotLastAdministrator(dbSession, user);
-    deleteRelatedData(dbSession, user);
-    return user;
-  }
-
-  private UserDto getUserOrThrow(DbSession dbSession, String login) {
-    UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
-    return checkFound(user, "User '%s' doesn't exist", login);
-  }
-
-  private void ensureNotLastAdministrator(DbSession dbSession, UserDto user) {
-    boolean isLastAdmin = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingUser(dbSession, ADMINISTER.getKey(), user.getUuid()) == 0;
-    checkRequest(!isLastAdmin, "User is last administrator, and cannot be deactivated");
-  }
-
-  private void deleteRelatedData(DbSession dbSession, UserDto user) {
-    String userUuid = user.getUuid();
-    dbClient.userTokenDao().deleteByUser(dbSession, user);
-    dbClient.propertiesDao().deleteByKeyAndValue(dbSession, DEFAULT_ISSUE_ASSIGNEE, user.getLogin());
-    dbClient.propertiesDao().deleteByQuery(dbSession, PropertyQuery.builder().setUserUuid(userUuid).build());
-    dbClient.userGroupDao().deleteByUserUuid(dbSession, user);
-    dbClient.userPermissionDao().deleteByUserUuid(dbSession, user);
-    dbClient.permissionTemplateDao().deleteUserPermissionsByUserUuid(dbSession, userUuid, user.getLogin());
-    dbClient.qProfileEditUsersDao().deleteByUser(dbSession, user);
-    dbClient.almPatDao().deleteByUser(dbSession, user);
-    dbClient.sessionTokensDao().deleteByUser(dbSession, user);
-    dbClient.userDismissedMessagesDao().deleteByUser(dbSession, user);
-    dbClient.qualityGateUserPermissionDao().deleteByUser(dbSession, user);
-  }
-
-  private void anonymizeUser(DbSession dbSession, UserDto user) {
-    userAnonymizer.anonymize(dbSession, user);
-    dbClient.userDao().update(dbSession, user);
-    dbClient.scimUserDao().deleteByUserUuid(dbSession, user.getUuid());
-  }
-
-  private void deactivateUser(DbSession dbSession, UserDto user) {
-    dbClient.userDao().deactivateUser(dbSession, user);
-    dbSession.commit();
-  }
-}
index b625c8c3744b266f3866f967cc2512f6a5b620b7..936a6302ebf42580b8b168ea3729c77e927fc18b 100644 (file)
@@ -20,6 +20,8 @@
 package org.sonar.server.user.ws;
 
 import org.sonar.core.platform.Module;
+import org.sonar.server.common.user.UserAnonymizer;
+import org.sonar.server.common.user.UserDeactivator;
 import org.sonar.server.common.user.service.UserService;
 
 public class UsersWsModule extends Module {
index dfd97224ee690f29c8ea2393370336dbbd1e1ec5..abc7d8577fd5c03b75ce23fc62204cc918a7ff4a 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserGroupDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UserSession;
 
 import static java.lang.String.format;
index cfc53523bfb600f1ccc3230a95d891d1e6f45f38..a728d4704a98d17dff4df7003c407304df61c8a0 100644 (file)
@@ -27,7 +27,7 @@ import org.sonar.api.server.ws.WebService.NewController;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.user.GroupDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.UserGroups;
 
index 7e4d301fd7612a1d63494d7f51b97bd53494713a..c0ca2d56bc8c45edd180b7024ef1bed9e63e448e 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.permission.GlobalPermission;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserDto;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UserSession;
 
 import static java.lang.String.format;
index e67a3ae888780201e1337296391ce0af5eabd054..8113fa13afd27e1a17ff95bbc9589953dec81b60 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.user.GroupDto;
 import org.sonar.db.user.UserMembershipQuery;
 import org.sonar.server.exceptions.NotFoundException;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.UserGroups;
 
index b4dd9c03a1f7ae2a2b1344dac9695e11ed347ef5..22ea5abe0bfd1d98f3d121c184f655d31e5fe4f8 100644 (file)
@@ -20,7 +20,7 @@
 package org.sonar.server.usergroups.ws;
 
 import org.sonar.core.platform.Module;
-import org.sonar.server.management.ManagedInstanceChecker;
+import org.sonar.server.common.management.ManagedInstanceChecker;
 
 public class UserGroupsModule extends Module {
 
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/management/ManagedInstanceCheckerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/management/ManagedInstanceCheckerTest.java
deleted file mode 100644 (file)
index 1695984..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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.management;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.sonar.db.DbSession;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.db.user.GroupDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.exceptions.BadRequestException;
-
-import static org.assertj.core.api.Assertions.assertThatNoException;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class ManagedInstanceCheckerTest {
-
-  private static final String INSTANCE_EXCEPTION_MESSAGE = "Operation not allowed when the instance is externally managed.";
-  private static final String PROJECT_EXCEPTION_MESSAGE = "Operation not allowed when the project is externally managed.";
-
-  @Mock
-  private DbSession dbSession;
-  @Mock
-  private ManagedInstanceService managedInstanceService;
-  @Mock
-  private ManagedProjectService managedProjectService;
-  @InjectMocks
-  private ManagedInstanceChecker managedInstanceChecker;
-
-  @Test
-  public void throwIfInstanceIsManaged_whenInstanceExternallyManaged_shouldThrow() {
-    when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(true);
-
-    assertThatThrownBy(() -> managedInstanceChecker.throwIfInstanceIsManaged())
-      .isInstanceOf(BadRequestException.class)
-      .hasMessage(INSTANCE_EXCEPTION_MESSAGE);
-  }
-
-  @Test
-  public void throwIfInstanceIsManaged_whenInstanceNotExternallyManaged_shouldNotThrow() {
-    when(managedInstanceService.isInstanceExternallyManaged()).thenReturn(false);
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfInstanceIsManaged());
-  }
-
-  @Test
-  public void throwIfProjectIsManaged_whenProjectIsManaged_shouldThrow() {
-    ProjectDto projectDto = mockManagedProject();
-
-    String projectUuid = projectDto.getUuid();
-    assertThatThrownBy(() -> managedInstanceChecker.throwIfProjectIsManaged(dbSession, projectUuid))
-      .isInstanceOf(BadRequestException.class)
-      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
-  }
-
-  @Test
-  public void throwIfProjectIsManaged_whenProjectIsNotManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockNotManagedProject();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfProjectIsManaged(dbSession, projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfUserIsManaged_whenUserIsManaged_shouldThrow() {
-    UserDto userDto = mockManagedUser();
-
-    String userUuid = userDto.getUuid();
-    assertThatThrownBy(() -> managedInstanceChecker.throwIfUserIsManaged(dbSession, userUuid))
-      .isInstanceOf(BadRequestException.class)
-      .hasMessage(INSTANCE_EXCEPTION_MESSAGE);
-  }
-
-  @Test
-  public void throwIfUserIsManaged_whenUserIsNotManaged_shouldNotThrow() {
-    UserDto userDto = mockNotManagedUser();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserIsManaged(dbSession, userDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfUserAndProjectAreManaged_whenUserAndProjectAreManaged_shouldThrow() {
-    ProjectDto projectDto = mockManagedProject();
-    UserDto userDto = mockManagedUser();
-
-    String userUuid = userDto.getUuid();
-    String projectUuid = projectDto.getUuid();
-    assertThatThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userUuid, projectUuid))
-      .isInstanceOf(BadRequestException.class)
-      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
-  }
-
-  @Test
-  public void throwIfUserAndProjectAreManaged_whenOnlyUserIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockNotManagedProject();
-    UserDto userDto = mockManagedUser();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfUserAndProjectAreManaged_whenOnlyProjectIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockManagedProject();
-    UserDto userDto = mockNotManagedUser();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfUserAndProjectAreManaged_whenNothingIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockNotManagedProject();
-    UserDto userDto = mockNotManagedUser();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfUserAndProjectAreManaged(dbSession, userDto.getUuid(), projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfGroupAndProjectAreManaged_whenGroupAndProjectAreManaged_shouldThrow() {
-    ProjectDto projectDto = mockManagedProject();
-    GroupDto groupDto = mockManagedGroup();
-
-    String groupDtoUuid = groupDto.getUuid();
-    String projectDtoUuid = projectDto.getUuid();
-    assertThatThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDtoUuid, projectDtoUuid))
-      .isInstanceOf(BadRequestException.class)
-      .hasMessage(PROJECT_EXCEPTION_MESSAGE);
-  }
-
-  @Test
-  public void throwIfGroupAndProjectAreManaged_whenOnlyGroupIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockNotManagedProject();
-    GroupDto groupDto = mockManagedGroup();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfGroupAndProjectAreManaged_whenOnlyProjectIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockManagedProject();
-    GroupDto groupDto = mockNotManagedGroup();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
-  }
-
-  @Test
-  public void throwIfGroupAndProjectAreManaged_whenNothingIsManaged_shouldNotThrow() {
-    ProjectDto projectDto = mockNotManagedProject();
-    GroupDto groupDto = mockNotManagedGroup();
-
-    assertThatNoException().isThrownBy(() -> managedInstanceChecker.throwIfGroupAndProjectAreManaged(dbSession, groupDto.getUuid(), projectDto.getUuid()));
-  }
-
-  private ProjectDto mockManagedProject() {
-    return mockProject(true);
-  }
-
-  private ProjectDto mockNotManagedProject() {
-    return mockProject(false);
-  }
-
-  private ProjectDto mockProject(boolean isManaged) {
-    ProjectDto projectDto = mock(ProjectDto.class);
-    when(managedProjectService.isProjectManaged(dbSession, projectDto.getUuid())).thenReturn(isManaged);
-    return projectDto;
-  }
-
-  private UserDto mockManagedUser() {
-    return mockUser(true);
-  }
-
-  private UserDto mockNotManagedUser() {
-    return mockUser(false);
-  }
-
-  private UserDto mockUser(boolean isManaged) {
-    UserDto userDto = mock(UserDto.class);
-    when(managedInstanceService.isUserManaged(dbSession, userDto.getUuid())).thenReturn(isManaged);
-    return userDto;
-  }
-
-  private GroupDto mockManagedGroup() {
-    return mockGroup(true);
-  }
-
-  private GroupDto mockNotManagedGroup() {
-    return mockGroup(false);
-  }
-
-  private GroupDto mockGroup(boolean isManaged) {
-    GroupDto groupDto = mock(GroupDto.class);
-    when(managedInstanceService.isGroupManaged(dbSession, groupDto.getUuid())).thenReturn(isManaged);
-    return groupDto;
-  }
-
-}