]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17242 Add 'anonymize' parameter to 'users/deactivate'
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Tue, 30 Aug 2022 18:38:09 +0000 (13:38 -0500)
committersonartech <sonartech@sonarsource.com>
Fri, 2 Sep 2022 20:02:50 +0000 (20:02 +0000)
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/DeactivateAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UserAnonymizer.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UsersWsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/AnonymizeActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UserAnonymizerTest.java [new file with mode: 0644]

index 5f6b25a65b3a3d45afc6de9b8fed4952ef3b6fc2..3d1189456e9a6d3ea405bf5fce82c64371cde224 100644 (file)
@@ -19,9 +19,6 @@
  */
 package org.sonar.server.user.ws;
 
-import java.util.function.Supplier;
-import javax.inject.Inject;
-import org.apache.commons.lang.RandomStringUtils;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
@@ -29,31 +26,24 @@ 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.user.ExternalIdentity;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.user.index.UserIndexer;
 
 import static org.sonar.server.exceptions.NotFoundException.checkFound;
 
 public class AnonymizeAction implements UsersWsAction {
-  private static final int LOGIN_RANDOM_LENGTH = 6;
   private static final String PARAM_LOGIN = "login";
 
   private final DbClient dbClient;
   private final UserIndexer userIndexer;
   private final UserSession userSession;
-  private final Supplier<String> randomNameGenerator;
+  private final UserAnonymizer userAnonymizer;
 
-  @Inject
-  public AnonymizeAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession) {
-    this(dbClient, userIndexer, userSession, () -> "sq-removed-" + RandomStringUtils.randomAlphanumeric(LOGIN_RANDOM_LENGTH));
-  }
-
-  public AnonymizeAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession, Supplier<String> randomNameGenerator) {
+  public AnonymizeAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession, UserAnonymizer userAnonymizer) {
+    this.userAnonymizer = userAnonymizer;
     this.dbClient = dbClient;
     this.userIndexer = userIndexer;
     this.userSession = userSession;
-    this.randomNameGenerator = randomNameGenerator;
   }
 
   @Override
@@ -82,14 +72,7 @@ public class AnonymizeAction implements UsersWsAction {
         throw new IllegalArgumentException(String.format("User '%s' is not deactivated", login));
       }
 
-      String newLogin = generateAnonymousLogin(dbSession);
-      user
-        .setLogin(newLogin)
-        .setName(newLogin)
-        .setExternalIdentityProvider(ExternalIdentity.SQ_AUTHORITY)
-        .setLocal(true)
-        .setExternalId(newLogin)
-        .setExternalLogin(newLogin);
+      userAnonymizer.anonymize(dbSession, user);
       dbClient.userDao().update(dbSession, user);
       userIndexer.commitAndIndex(dbSession, user);
     }
@@ -97,13 +80,4 @@ public class AnonymizeAction implements UsersWsAction {
     response.noContent();
   }
 
-  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");
-  }
 }
index 480264d492178f3fe9e5820e9ae9acc1a2be5513..b8428a30331b4baae50af8963e88885f8ef45036 100644 (file)
@@ -42,17 +42,20 @@ import static org.sonar.server.exceptions.NotFoundException.checkFound;
 public class DeactivateAction implements UsersWsAction {
 
   private static final String PARAM_LOGIN = "login";
+  private static final String PARAM_ANONYMIZE = "anonymize";
 
   private final DbClient dbClient;
   private final UserIndexer userIndexer;
   private final UserSession userSession;
   private final UserJsonWriter userWriter;
+  private final UserAnonymizer userAnonymizer;
 
-  public DeactivateAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession, UserJsonWriter userWriter) {
+  public DeactivateAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession, UserJsonWriter userWriter, UserAnonymizer userAnonymizer) {
     this.dbClient = dbClient;
     this.userIndexer = userIndexer;
     this.userSession = userSession;
     this.userWriter = userWriter;
+    this.userAnonymizer = userAnonymizer;
   }
 
   @Override
@@ -68,6 +71,13 @@ public class DeactivateAction implements UsersWsAction {
       .setDescription("User login")
       .setRequired(true)
       .setExampleValue("myuser");
+
+    action.createParam(PARAM_ANONYMIZE)
+      .setDescription("Anonymize user in addition to deactivating it")
+      .setBooleanPossibleValues()
+      .setRequired(false)
+      .setSince("9.7")
+      .setDefaultValue(false);
   }
 
   @Override
@@ -96,11 +106,17 @@ public class DeactivateAction implements UsersWsAction {
       dbClient.sessionTokensDao().deleteByUser(dbSession, user);
       dbClient.userDismissedMessagesDao().deleteByUser(dbSession, user);
       dbClient.qualityGateUserPermissionDao().deleteByUser(dbSession, user);
+
+      if (request.mandatoryParamAsBoolean(PARAM_ANONYMIZE)) {
+        userAnonymizer.anonymize(dbSession, user);
+        dbClient.userDao().update(dbSession, user);
+      }
+
       dbClient.userDao().deactivateUser(dbSession, user);
+
       userIndexer.commitAndIndex(dbSession, user);
+      writeResponse(response, user.getLogin());
     }
-
-    writeResponse(response, login);
   }
 
   private void writeResponse(Response response, String login) {
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
new file mode 100644 (file)
index 0000000..873df0b
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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");
+  }
+}
index 6cff442be80d88dfca309018b5fab38f32c80137..0e6c86eff99d9ea90c270b3ad8f14e8b29556d1b 100644 (file)
@@ -41,6 +41,7 @@ public class UsersWsModule extends Module {
       UserJsonWriter.class,
       SetHomepageAction.class,
       HomepageTypesImpl.class,
+      UserAnonymizer.class,
       UpdateIdentityProviderAction.class,
       DismissNoticeAction.class);
 
index 09a7870694a6f7e67c53d5b5f520718b7c4e060c..993fc1d1c5b78a9cd98f0153ea5be0d8d261ba8d 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.user.ws;
 
-import java.util.Iterator;
 import java.util.List;
 import java.util.Optional;
 import javax.annotation.Nullable;
@@ -68,7 +67,8 @@ public class AnonymizeActionTest {
 
   private final DbClient dbClient = db.getDbClient();
   private final UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
-  private final WsActionTester ws = new WsActionTester(new AnonymizeAction(dbClient, userIndexer, userSession));
+  private final UserAnonymizer userAnonymizer = new UserAnonymizer(db.getDbClient());
+  private final WsActionTester ws = new WsActionTester(new AnonymizeAction(dbClient, userIndexer, userSession, userAnonymizer));
 
   @Test
   public void anonymize_user() {
@@ -83,28 +83,11 @@ public class AnonymizeActionTest {
       .setExternalId("external.id"));
     logInAsSystemAdministrator();
 
-    anonymize(user.getLogin());
+    TestResponse response = anonymize(user.getLogin());
 
     verifyThatUserIsAnonymized(user.getUuid());
     verifyThatUserIsAnonymizedOnEs(user.getUuid());
-  }
-
-  @Test
-  public void try_avoid_login_collisions() {
-    List<String> logins = List.of("login1", "login2", "login3");
-    Iterator<String> randomGeneratorIt = logins.iterator();
-    WsActionTester ws = new WsActionTester(new AnonymizeAction(dbClient, userIndexer, userSession, randomGeneratorIt::next));
-
-    UserDto user1 = db.users().insertUser(u -> u.setLogin("login1"));
-    UserDto user2 = db.users().insertUser(u -> u.setLogin("login2"));
-    UserDto userToAnonymize = db.users().insertUser(u -> u.setLogin("toAnonymize").setActive(false));
-
-    logInAsSystemAdministrator();
-
-    anonymize(ws, userToAnonymize.getLogin());
-    assertThat(dbClient.userDao().selectUsers(db.getSession(), UserQuery.builder().includeDeactivated().build()))
-      .extracting(UserDto::getLogin)
-      .containsOnly("login1", "login2", "login3");
+    assertThat(response.getInput()).isEmpty();
   }
 
   @Test
index a6ee09f9eac1d2e9f1dcc95c728300201b010c94..ab3c6f72531e0ac0f979f53509fd0f533c588c96 100644 (file)
@@ -50,6 +50,7 @@ import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
 import org.sonar.server.exceptions.UnauthorizedException;
 import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.user.ExternalIdentity;
 import org.sonar.server.user.index.UserIndexDefinition;
 import org.sonar.server.user.index.UserIndexer;
 import org.sonar.server.ws.TestRequest;
@@ -61,6 +62,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.sonar.api.web.UserRole.CODEVIEWER;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
@@ -85,7 +90,8 @@ public class DeactivateActionTest {
   private final DbClient dbClient = db.getDbClient();
   private final UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
   private final DbSession dbSession = db.getSession();
-  private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession, new UserJsonWriter(userSession)));
+  private final UserAnonymizer userAnonymizer = new UserAnonymizer(db.getDbClient(), () -> "anonymized");
+  private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession, new UserJsonWriter(userSession), userAnonymizer));
 
   @Test
   public void deactivate_user_and_delete_their_related_data() {
@@ -101,10 +107,32 @@ public class DeactivateActionTest {
 
     verifyThatUserIsDeactivated(user.getLogin());
     assertThat(es.client().search(EsClient.prepareSearch(UserIndexDefinition.TYPE_USER)
-      .source(new SearchSourceBuilder()
-        .query(boolQuery()
-          .must(termQuery(FIELD_UUID, user.getUuid()))
-          .must(termQuery(FIELD_ACTIVE, "false")))))
+        .source(new SearchSourceBuilder()
+          .query(boolQuery()
+            .must(termQuery(FIELD_UUID, user.getUuid()))
+            .must(termQuery(FIELD_ACTIVE, "false")))))
+      .getHits().getHits()).hasSize(1);
+  }
+
+  @Test
+  public void anonymize_user_if_param_provided() {
+    createAdminUser();
+    UserDto user = db.users().insertUser(u -> u
+      .setLogin("ada.lovelace")
+      .setEmail("ada.lovelace@noteg.com")
+      .setName("Ada Lovelace")
+      .setScmAccounts(singletonList("al")));
+    logInAsSystemAdministrator();
+
+    deactivate(user.getLogin(), true);
+
+    verifyThatUserIsDeactivated("anonymized");
+    verifyThatUserIsAnomymized("anonymized");
+    assertThat(es.client().search(EsClient.prepareSearch(UserIndexDefinition.TYPE_USER)
+        .source(new SearchSourceBuilder()
+          .query(boolQuery()
+            .must(termQuery(FIELD_UUID, user.getUuid()))
+            .must(termQuery(FIELD_ACTIVE, "false")))))
       .getHits().getHits()).hasSize(1);
   }
 
@@ -394,7 +422,7 @@ public class DeactivateActionTest {
   public void test_definition() {
     assertThat(ws.getDef().isPost()).isTrue();
     assertThat(ws.getDef().isInternal()).isFalse();
-    assertThat(ws.getDef().params()).hasSize(1);
+    assertThat(ws.getDef().params()).hasSize(2);
   }
 
   @Test
@@ -418,13 +446,20 @@ public class DeactivateActionTest {
   }
 
   private TestResponse deactivate(@Nullable String login) {
-    return deactivate(ws, login);
+    return deactivate(login, false);
+  }
+
+  private TestResponse deactivate(@Nullable String login, boolean anonymize) {
+    return deactivate(ws, login, anonymize);
   }
 
-  private TestResponse deactivate(WsActionTester ws, @Nullable String login) {
+  private TestResponse deactivate(WsActionTester ws, @Nullable String login, boolean anonymize) {
     TestRequest request = ws.newRequest()
       .setMethod("POST");
     Optional.ofNullable(login).ifPresent(t -> request.setParam("login", login));
+    if (anonymize) {
+      request.setParam("anonymize", "true");
+    }
     return request.execute();
   }
 
@@ -440,6 +475,15 @@ public class DeactivateActionTest {
     assertThat(user.get().getScmAccountsAsList()).isEmpty();
   }
 
+  private void verifyThatUserIsAnomymized(String login) {
+    Optional<UserDto> user = db.users().selectUserByLogin(login);
+    assertThat(user).isPresent();
+    assertThat(user.get().getName()).isEqualTo(login);
+    assertThat(user.get().getExternalLogin()).isEqualTo(login);
+    assertThat(user.get().getExternalId()).isEqualTo(login);
+    assertThat(user.get().getExternalIdentityProvider()).isEqualTo(ExternalIdentity.SQ_AUTHORITY);
+  }
+
   private UserDto createAdminUser() {
     UserDto admin = db.users().insertUser();
     db.users().insertPermissionOnUser(admin, ADMINISTER);
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UserAnonymizerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UserAnonymizerTest.java
new file mode 100644 (file)
index 0000000..c2b66a3
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.Iterator;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+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.user.ExternalIdentity;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UserAnonymizerTest {
+  private final System2 system2 = new AlwaysIncreasingSystem2();
+  @Rule
+  public DbTester db = DbTester.create(system2);
+  private final UserAnonymizer userAnonymizer = new UserAnonymizer(db.getDbClient());
+
+  @Test
+  public void anonymize_user() {
+    UserDto user = db.users().insertUser(u -> u.setLogin("login1"));
+    userAnonymizer.anonymize(db.getSession(), user);
+    assertThat(user.getLogin()).startsWith("sq-removed-");
+    assertThat(user.getExternalIdentityProvider()).isEqualTo(ExternalIdentity.SQ_AUTHORITY);
+    assertThat(user.getExternalId()).isEqualTo(user.getLogin());
+    assertThat(user.getExternalLogin()).isEqualTo(user.getLogin());
+    assertThat(user.getName()).isEqualTo(user.getLogin());
+  }
+
+  @Test
+  public void try_avoid_login_collisions() {
+    List<String> logins = List.of("login1", "login2", "login3");
+    Iterator<String> randomGeneratorIt = logins.iterator();
+    UserAnonymizer userAnonymizer = new UserAnonymizer(db.getDbClient(), randomGeneratorIt::next);
+
+    UserDto user1 = db.users().insertUser(u -> u.setLogin("login1"));
+    UserDto user2 = db.users().insertUser(u -> u.setLogin("login2"));
+    UserDto userToAnonymize = db.users().insertUser(u -> u.setLogin("toAnonymize").setActive(false));
+
+    userAnonymizer.anonymize(db.getSession(), userToAnonymize);
+    assertThat(userToAnonymize.getLogin()).isEqualTo("login3");
+  }
+
+}