*/
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;
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
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);
}
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");
- }
}
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
.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
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) {
--- /dev/null
+/*
+ * 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");
+ }
+}
UserJsonWriter.class,
SetHomepageAction.class,
HomepageTypesImpl.class,
+ UserAnonymizer.class,
UpdateIdentityProviderAction.class,
DismissNoticeAction.class);
*/
package org.sonar.server.user.ws;
-import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
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() {
.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
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;
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;
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() {
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);
}
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
}
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();
}
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);
--- /dev/null
+/*
+ * 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");
+ }
+
+}