You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/05/25 05:45:21 UTC
[james-project] 01/03: JAMES-3909 Task + Webadmin route for delete all users data of a domain
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 79092f21427011722b656fca5c58b5044235424f
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Fri May 19 17:18:51 2023 +0700
JAMES-3909 Task + Webadmin route for delete all users data of a domain
---
.../docs/modules/ROOT/pages/operate/webadmin.adoc | 27 +++
.../james/modules/server/DataRoutesModules.java | 20 +++
.../james/webadmin/routes/DomainsRoutes.java | 33 +++-
.../webadmin/service/DeleteUserDataService.java | 4 +
.../service/DeleteUsersDataOfDomainTask.java | 173 ++++++++++++++++++
...rsDataOfDomainTaskAdditionalInformationDTO.java | 81 +++++++++
.../service/DeleteUsersDataOfDomainTaskDTO.java | 68 ++++++++
.../james/webadmin/routes/DomainsRoutesTest.java | 174 +++++++++++++++++-
...leteUsersDataOfDomainTaskSerializationTest.java | 93 ++++++++++
.../service/DeleteUsersDataOfDomainTaskTest.java | 194 +++++++++++++++++++++
src/site/markdown/server/manage-webadmin.md | 28 +++
11 files changed, 892 insertions(+), 3 deletions(-)
diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
index 1a343c7d30..0a69f5618f 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
@@ -455,6 +455,33 @@ syntax
* 400: source, domain and destination domain are the same
* 404: `source.domain.tld` are not part of handled domains.
+=== Delete all users data of a domain
+
+....
+curl -XPOST http://ip:port/domains/{domainToBeUsed}?action=deleteData
+....
+
+Would create a task that deletes data of all users of the domain.
+
+[More details about endpoints returning a task](#_endpoints_returning_a_task).
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Error in the request. Details can be found in the reported error.
+
+The scheduled task will have the following type `DeleteUsersDataOfDomainTask` and the following `additionalInformation`:
+
+....
+{
+ "type": "DeleteUsersDataOfDomainTask",
+ "domain": "domain.tld",
+ "successfulUsersCount": 2,
+ "failedUsersCount": 1,
+ "timestamp": "2023-05-22T08:52:47.076261Z"
+}
+....
+
== Administrating users
=== Create a user
diff --git a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
index 5b3addf079..48fcf5efe2 100644
--- a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
+++ b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
@@ -25,6 +25,7 @@ import org.apache.james.server.task.json.dto.TaskDTO;
import org.apache.james.server.task.json.dto.TaskDTOModule;
import org.apache.james.task.Task;
import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.webadmin.Routes;
import org.apache.james.webadmin.dto.DTOModuleInjections;
import org.apache.james.webadmin.dto.MappingSourceModule;
@@ -44,6 +45,8 @@ import org.apache.james.webadmin.routes.UsernameChangeRoutes;
import org.apache.james.webadmin.service.DeleteUserDataService;
import org.apache.james.webadmin.service.DeleteUserDataTaskAdditionalInformationDTO;
import org.apache.james.webadmin.service.DeleteUserDataTaskDTO;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskDTO;
import org.apache.james.webadmin.service.UsernameChangeService;
import org.apache.james.webadmin.service.UsernameChangeTaskAdditionalInformationDTO;
import org.apache.james.webadmin.service.UsernameChangeTaskDTO;
@@ -108,4 +111,21 @@ public class DataRoutesModules extends AbstractModule {
public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> webAdminDeleteUserDataTaskAdditionalInformationDTO() {
return DeleteUserDataTaskAdditionalInformationDTO.module();
}
+
+ // delete all users data of a domain DTO modules
+ @ProvidesIntoSet
+ public TaskDTOModule<? extends Task, ? extends TaskDTO> deleteUsersDataOfDomainTaskDTO(DeleteUserDataService service, UsersRepository usersRepository) {
+ return DeleteUsersDataOfDomainTaskDTO.module(service, usersRepository);
+ }
+
+ @ProvidesIntoSet
+ public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> deleteUsersDataOfDomainTaskAdditionalInformationDTO() {
+ return DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module();
+ }
+
+ @Named(DTOModuleInjections.WEBADMIN_DTO)
+ @ProvidesIntoSet
+ public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> webAdminDeleteUsersDataOfDomainTaskAdditionalInformationDTO() {
+ return DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module();
+ }
}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index 59ae633c61..34ef2f9ab9 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -34,9 +34,15 @@ import org.apache.james.domainlist.api.DomainList;
import org.apache.james.domainlist.api.DomainListException;
import org.apache.james.rrt.api.RecipientRewriteTableException;
import org.apache.james.rrt.api.SameSourceAndDestinationException;
+import org.apache.james.task.TaskManager;
+import org.apache.james.user.api.UsersRepository;
import org.apache.james.webadmin.Routes;
import org.apache.james.webadmin.dto.DomainAliasResponse;
+import org.apache.james.webadmin.service.DeleteUserDataService;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTask;
import org.apache.james.webadmin.service.DomainAliasService;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.tasks.TaskRegistrationKey;
import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
import org.apache.james.webadmin.utils.JsonTransformer;
@@ -45,11 +51,13 @@ import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import spark.HaltException;
import spark.Request;
import spark.Response;
+import spark.Route;
import spark.Service;
public class DomainsRoutes implements Routes {
@@ -62,18 +70,26 @@ public class DomainsRoutes implements Routes {
private static final String SPECIFIC_DOMAIN = DOMAINS + SEPARATOR + DOMAIN_NAME;
private static final String ALIASES = "aliases";
private static final String DOMAIN_ALIASES = SPECIFIC_DOMAIN + SEPARATOR + ALIASES;
+ private static final String DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH = "/domains/:domainName";
private static final String SPECIFIC_ALIAS = DOMAINS + SEPARATOR + DESTINATION_DOMAIN + SEPARATOR + ALIASES + SEPARATOR + SOURCE_DOMAIN;
+ private static final TaskRegistrationKey DELETE_USERS_DATA = TaskRegistrationKey.of("deleteData");
private final DomainList domainList;
private final DomainAliasService domainAliasService;
private final JsonTransformer jsonTransformer;
+ private final DeleteUserDataService deleteUserDataService;
+ private final UsersRepository usersRepository;
+ private final TaskManager taskManager;
private Service service;
@Inject
- DomainsRoutes(DomainList domainList, DomainAliasService domainAliasService, JsonTransformer jsonTransformer) {
+ DomainsRoutes(DomainList domainList, DomainAliasService domainAliasService, JsonTransformer jsonTransformer, DeleteUserDataService deleteUserDataService, UsersRepository usersRepository, TaskManager taskManager) {
this.domainList = domainList;
this.domainAliasService = domainAliasService;
this.jsonTransformer = jsonTransformer;
+ this.deleteUserDataService = deleteUserDataService;
+ this.usersRepository = usersRepository;
+ this.taskManager = taskManager;
}
@Override
@@ -95,6 +111,21 @@ public class DomainsRoutes implements Routes {
defineListAliases(service);
defineAddAlias(service);
defineRemoveAlias(service);
+
+ // delete data of all users of a domain
+ service.post(DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH, deleteAllUsersData(), jsonTransformer);
+ }
+
+ public Route deleteAllUsersData() {
+ return TaskFromRequestRegistry.builder()
+ .parameterName("action")
+ .register(DELETE_USERS_DATA, request -> {
+ Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
+ Preconditions.checkArgument(domainList.containsDomain(domain), "'domainName' parameter should be an existing domain");
+
+ return new DeleteUsersDataOfDomainTask(deleteUserDataService, domain, usersRepository);
+ })
+ .buildAsRoute(taskManager);
}
public void defineDeleteDomain() {
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
index 2bd572b68c..e24987358d 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
@@ -143,4 +143,8 @@ public class DeleteUserDataService {
public Performer performer(Optional<StepName> fromStep) {
return new Performer(steps, new DeleteUserDataStatus(steps), fromStep);
}
+
+ public Performer performer() {
+ return new Performer(steps, new DeleteUserDataStatus(steps), Optional.empty());
+ }
}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java
new file mode 100644
index 0000000000..84487aade6
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java
@@ -0,0 +1,173 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.service;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+import org.apache.james.user.api.UsersRepository;
+import org.reactivestreams.Publisher;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DeleteUsersDataOfDomainTask implements Task {
+ static final TaskType TYPE = TaskType.of("DeleteUsersDataOfDomainTask");
+ private static final int LOW_CONCURRENCY = 2;
+
+ public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+ private final Instant timestamp;
+ private final Domain domain;
+ private final long successfulUsersCount;
+ private final long failedUsersCount;
+
+ public AdditionalInformation(Instant timestamp, Domain domain, long successfulUsersCount, long failedUsersCount) {
+ this.timestamp = timestamp;
+ this.domain = domain;
+ this.successfulUsersCount = successfulUsersCount;
+ this.failedUsersCount = failedUsersCount;
+ }
+
+ public Domain getDomain() {
+ return domain;
+ }
+
+ public long getSuccessfulUsersCount() {
+ return successfulUsersCount;
+ }
+
+ public long getFailedUsersCount() {
+ return failedUsersCount;
+ }
+
+ @Override
+ public Instant timestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof AdditionalInformation) {
+ AdditionalInformation that = (AdditionalInformation) o;
+
+ return Objects.equals(this.successfulUsersCount, that.successfulUsersCount)
+ && Objects.equals(this.failedUsersCount, that.failedUsersCount)
+ && Objects.equals(this.timestamp, that.timestamp)
+ && Objects.equals(this.domain, that.domain);
+ }
+ return false;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(timestamp, domain, successfulUsersCount, failedUsersCount);
+ }
+ }
+
+ static class Context {
+ private final AtomicLong successfulUsersCount;
+ private final AtomicLong failedUsersCount;
+
+ public Context() {
+ this.successfulUsersCount = new AtomicLong();
+ this.failedUsersCount = new AtomicLong();
+ }
+
+ private void increaseSuccessfulUsers() {
+ successfulUsersCount.incrementAndGet();
+ }
+
+ private void increaseFailedUsers() {
+ failedUsersCount.incrementAndGet();
+ }
+
+ public long getSuccessfulUsersCount() {
+ return successfulUsersCount.get();
+ }
+
+ public long getFailedUsersCount() {
+ return failedUsersCount.get();
+ }
+ }
+
+ private final Domain domain;
+ private final DeleteUserDataService deleteUserDataService;
+ private final UsersRepository usersRepository;
+ private final Context context;
+
+ public DeleteUsersDataOfDomainTask(DeleteUserDataService deleteUserDataService, Domain domain, UsersRepository usersRepository) {
+ this.deleteUserDataService = deleteUserDataService;
+ this.domain = domain;
+ this.usersRepository = usersRepository;
+ this.context = new Context();
+ }
+
+ @Override
+ public Result run() {
+ return Flux.from(usersRepository.listUsersOfADomainReactive(domain))
+ .flatMap(deleteUserData(), LOW_CONCURRENCY)
+ .reduce(Task::combine)
+ .switchIfEmpty(Mono.just(Result.COMPLETED))
+ .block();
+ }
+
+ private Function<Username, Publisher<Result>> deleteUserData() {
+ return username -> deleteUserDataService.performer().deleteUserData(username)
+ .then(Mono.fromCallable(() -> {
+ context.increaseSuccessfulUsers();
+ return Result.COMPLETED;
+ }))
+ .onErrorResume(error -> {
+ LOGGER.error("Error when deleting data of user {}", username.asString(), error);
+ context.increaseFailedUsers();
+ return Mono.just(Result.PARTIAL);
+ });
+ }
+
+ @Override
+ public TaskType type() {
+ return TYPE;
+ }
+
+ @Override
+ public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+ return Optional.of(new AdditionalInformation(Clock.systemUTC().instant(), domain, context.getSuccessfulUsersCount(), context.getFailedUsersCount()));
+ }
+
+ public Domain getDomain() {
+ return domain;
+ }
+
+ @VisibleForTesting
+ Context getContext() {
+ return context;
+ }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000000..7bcd3c033d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java
@@ -0,0 +1,81 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.service;
+
+import java.time.Instant;
+
+import org.apache.james.core.Domain;
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeleteUsersDataOfDomainTaskAdditionalInformationDTO implements AdditionalInformationDTO {
+ public static AdditionalInformationDTOModule<DeleteUsersDataOfDomainTask.AdditionalInformation, DeleteUsersDataOfDomainTaskAdditionalInformationDTO> module() {
+ return DTOModule.forDomainObject(DeleteUsersDataOfDomainTask.AdditionalInformation.class)
+ .convertToDTO(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.class)
+ .toDomainObjectConverter(dto -> new DeleteUsersDataOfDomainTask.AdditionalInformation(
+ dto.timestamp, Domain.of(dto.domain), dto.successfulUsersCount, dto.failedUsersCount))
+ .toDTOConverter((details, type) -> new DeleteUsersDataOfDomainTaskAdditionalInformationDTO(
+ type, details.getDomain().asString(), details.getSuccessfulUsersCount(), details.getFailedUsersCount(), details.timestamp()))
+ .typeName(DeleteUsersDataOfDomainTask.TYPE.asString())
+ .withFactory(AdditionalInformationDTOModule::new);
+ }
+
+ private final String type;
+ private final String domain;
+ private final long successfulUsersCount;
+ private final long failedUsersCount;
+ private final Instant timestamp;
+
+ public DeleteUsersDataOfDomainTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+ @JsonProperty("domain") String domain,
+ @JsonProperty("successfulUsersCount") long successfulUsersCount,
+ @JsonProperty("failedUsersCount") long failedUsersCount,
+ @JsonProperty("timestamp") Instant timestamp) {
+ this.type = type;
+ this.domain = domain;
+ this.successfulUsersCount = successfulUsersCount;
+ this.failedUsersCount = failedUsersCount;
+ this.timestamp = timestamp;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public long getSuccessfulUsersCount() {
+ return successfulUsersCount;
+ }
+
+ public long getFailedUsersCount() {
+ return failedUsersCount;
+ }
+
+ public Instant getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java
new file mode 100644
index 0000000000..df9f11bd6d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.service;
+
+import org.apache.james.core.Domain;
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.TaskDTO;
+import org.apache.james.server.task.json.dto.TaskDTOModule;
+import org.apache.james.user.api.UsersRepository;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeleteUsersDataOfDomainTaskDTO implements TaskDTO {
+
+ public static TaskDTOModule<DeleteUsersDataOfDomainTask, DeleteUsersDataOfDomainTaskDTO> module(DeleteUserDataService service, UsersRepository usersRepository) {
+ return DTOModule
+ .forDomainObject(DeleteUsersDataOfDomainTask.class)
+ .convertToDTO(DeleteUsersDataOfDomainTaskDTO.class)
+ .toDomainObjectConverter(dto -> dto.fromDTO(service, usersRepository))
+ .toDTOConverter(DeleteUsersDataOfDomainTaskDTO::toDTO)
+ .typeName(DeleteUsersDataOfDomainTask.TYPE.asString())
+ .withFactory(TaskDTOModule::new);
+ }
+
+ public static DeleteUsersDataOfDomainTaskDTO toDTO(DeleteUsersDataOfDomainTask domainObject, String typeName) {
+ return new DeleteUsersDataOfDomainTaskDTO(typeName,
+ domainObject.getDomain().asString());
+ }
+
+ private final String type;
+ private final String domain;
+
+ public DeleteUsersDataOfDomainTaskDTO(@JsonProperty("type") String type,
+ @JsonProperty("domain") String domain) {
+ this.type = type;
+ this.domain = domain;
+ }
+
+ public DeleteUsersDataOfDomainTask fromDTO(DeleteUserDataService service, UsersRepository usersRepository) {
+ return new DeleteUsersDataOfDomainTask(service, Domain.of(domain), usersRepository);
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index e4f0b9fe7b..baefeaf289 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -35,41 +35,97 @@ import static org.mockito.Mockito.when;
import java.net.InetAddress;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
import org.apache.james.dnsservice.api.DNSService;
import org.apache.james.domainlist.api.DomainList;
import org.apache.james.domainlist.api.DomainListException;
import org.apache.james.domainlist.lib.DomainListConfiguration;
import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.json.DTOConverter;
import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.memory.MemoryUsersRepository;
import org.apache.james.webadmin.WebAdminServer;
import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.service.DeleteUserDataService;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskAdditionalInformationDTO;
import org.apache.james.webadmin.service.DomainAliasService;
+import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.JsonTransformer;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
+import reactor.core.publisher.Mono;
class DomainsRoutesTest {
+ private static class RecordProcessedUsersStep implements DeleteUserDataTaskStep {
+ private final Set<Username> processedUsers = ConcurrentHashMap.newKeySet();
+
+ public RecordProcessedUsersStep() {
+ }
+
+ @Override
+ public StepName name() {
+ return new StepName("RecordProcessedUsersStep");
+ }
+
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ @Override
+ public Publisher<Void> deleteUserData(Username username) {
+ processedUsers.add(username);
+ return Mono.empty();
+ }
+ }
+
private static final String DOMAIN = "domain";
private static final String ALIAS_DOMAIN = "alias.domain";
private static final String ALIAS_DOMAIN_2 = "alias.domain.bis";
private static final String EXTERNAL_DOMAIN = "external.domain.tld";
private WebAdminServer webAdminServer;
+ private MemoryUsersRepository usersRepository;
private void createServer(DomainList domainList) {
+ MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
+ DomainAliasService domainAliasService = new DomainAliasService(new MemoryRecipientRewriteTable(), domainList);
+ usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+ webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer(),
+ new DeleteUserDataService(Set.of()), usersRepository, taskManager), new TasksRoutes(taskManager, new JsonTransformer(), DTOConverter.of(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())))
+ .start();
+
+ RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+ .setBasePath(DomainsRoutes.DOMAINS)
+ .build();
+ }
+
+ private void createServer(DomainList domainList, Set<DeleteUserDataTaskStep> steps) {
+ MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
DomainAliasService domainAliasService = new DomainAliasService(new MemoryRecipientRewriteTable(), domainList);
- webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer()))
+ DeleteUserDataService deleteUserDataService = new DeleteUserDataService(steps);
+ usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+ webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer(), deleteUserDataService, usersRepository, taskManager),
+ new TasksRoutes(taskManager, new JsonTransformer(), DTOConverter.of(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())))
.start();
RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
@@ -82,6 +138,120 @@ class DomainsRoutesTest {
webAdminServer.destroy();
}
+ @Nested
+ class DeleteAllUsersDataTests {
+ private RecordProcessedUsersStep recordProcessedUsersStep;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ DNSService dnsService = mock(DNSService.class);
+ when(dnsService.getHostName(any())).thenReturn("localhost");
+ when(dnsService.getLocalHost()).thenReturn(InetAddress.getByName("localhost"));
+ MemoryDomainList domainList = new MemoryDomainList(dnsService);
+ domainList.configure(DomainListConfiguration.builder()
+ .autoDetect(false)
+ .autoDetectIp(false)
+ .build());
+ domainList.addDomain(Domain.of("domain.tld"));
+
+ recordProcessedUsersStep = new RecordProcessedUsersStep();
+ Set<DeleteUserDataTaskStep> steps = ImmutableSet.of(new DeleteUserDataRoutesTest.StepImpl(new DeleteUserDataTaskStep.StepName("A"), 35, Mono.empty()),
+ recordProcessedUsersStep);
+ createServer(domainList, steps);
+ }
+
+ @Test
+ void shouldDeleteAllUsersDataOfTheDomain() throws UsersRepositoryException {
+ // GIVEN localhost domain has 2 users
+ usersRepository.addUser(Username.of("user1@localhost"), "secret");
+ usersRepository.addUser(Username.of("user2@localhost"), "secret");
+
+ // THEN delete all users data of localhost domain
+ String taskId = with()
+ .basePath("/domains")
+ .queryParam("action", "deleteData")
+ .post("/localhost")
+ .jsonPath()
+ .get("taskId");
+
+ given()
+ .basePath(TasksRoutes.BASE)
+ .when()
+ .get(taskId + "/await")
+ .then()
+ .body("type", is("DeleteUsersDataOfDomainTask"))
+ .body("status", is("completed"))
+ .body("additionalInformation.type", is("DeleteUsersDataOfDomainTask"))
+ .body("additionalInformation.domain", is("localhost"))
+ .body("additionalInformation.successfulUsersCount", is(2))
+ .body("additionalInformation.failedUsersCount", is(0));
+
+ // then should delete data of the 2 users
+ assertThat(recordProcessedUsersStep.processedUsers)
+ .containsExactlyInAnyOrder(Username.of("user1@localhost"), Username.of("user2@localhost"));
+ }
+
+ @Test
+ void shouldFailWhenInvalidAction() {
+ given()
+ .basePath("/domains")
+ .queryParam("action", "invalid")
+ .post("/localhost")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .body("statusCode", is(400))
+ .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+ .body("message", is("Invalid arguments supplied in the user request"))
+ .body("details", is("Invalid value supplied for query parameter 'action': invalid. Supported values are [deleteData]"));
+ }
+
+ @Test
+ void shouldFailWhenNonExistingDomain() {
+ given()
+ .basePath("/domains")
+ .queryParam("action", "deleteData")
+ .post("/nonExistingDomain")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .body("statusCode", is(400))
+ .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+ .body("message", is("Invalid arguments supplied in the user request"))
+ .body("details", is("'domainName' parameter should be an existing domain"));
+ }
+
+ @Test
+ void shouldNotDeleteUsersDataOfOtherDomains() throws UsersRepositoryException {
+ // GIVEN localhost domain has 2 users and domain.tld domain has 1 user
+ usersRepository.addUser(Username.of("user1@localhost"), "secret");
+ usersRepository.addUser(Username.of("user2@localhost"), "secret");
+ usersRepository.addUser(Username.of("user3@domain.tld"), "secret");
+
+ // WHEN delete users data of localhost domain
+ String taskId = with()
+ .basePath("/domains")
+ .queryParam("action", "deleteData")
+ .post("/localhost")
+ .jsonPath()
+ .get("taskId");
+
+ given()
+ .basePath(TasksRoutes.BASE)
+ .when()
+ .get(taskId + "/await")
+ .then()
+ .body("type", is("DeleteUsersDataOfDomainTask"))
+ .body("status", is("completed"))
+ .body("additionalInformation.type", is("DeleteUsersDataOfDomainTask"))
+ .body("additionalInformation.domain", is("localhost"))
+ .body("additionalInformation.successfulUsersCount", is(2))
+ .body("additionalInformation.failedUsersCount", is(0));
+
+ // THEN users data of domain.tld should not be clear
+ assertThat(recordProcessedUsersStep.processedUsers)
+ .doesNotContain(Username.of("user3@domain.tld"));
+ }
+ }
+
@Nested
class NormalBehaviour {
@@ -433,7 +603,7 @@ class DomainsRoutesTest {
with().put(DOMAIN);
when()
- .put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN).prettyPeek()
+ .put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN)
.then()
.contentType(ContentType.JSON)
.statusCode(HttpStatus.NO_CONTENT_204);
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java
new file mode 100644
index 0000000000..8de0149656
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java
@@ -0,0 +1,93 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.service;
+
+import static org.mockito.Mockito.mock;
+
+import java.time.Instant;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepository;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Mono;
+
+class DeleteUsersDataOfDomainTaskSerializationTest {
+ private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+ private static final Domain DOMAIN = Domain.of("domain");
+ private static final long SUCCESSFUL_USERS_COUNT = 99L;
+ private static final long FAILED_USERS_COUNT = 1L;
+ private static final DeleteUserDataTaskStep.StepName STEP_A = new DeleteUserDataTaskStep.StepName("A");
+ private static final DeleteUserDataTaskStep.StepName STEP_B = new DeleteUserDataTaskStep.StepName("B");
+ private static final DeleteUserDataTaskStep.StepName STEP_C = new DeleteUserDataTaskStep.StepName("C");
+ private static final DeleteUserDataTaskStep.StepName STEP_D = new DeleteUserDataTaskStep.StepName("D");
+ private static final DeleteUserDataTaskStep A = asStep(STEP_A);
+ private static final DeleteUserDataTaskStep B = asStep(STEP_B);
+ private static final DeleteUserDataTaskStep C = asStep(STEP_C);
+ private static final DeleteUserDataTaskStep D = asStep(STEP_D);
+
+ private static DeleteUserDataTaskStep asStep(DeleteUserDataTaskStep.StepName name) {
+ return new DeleteUserDataTaskStep() {
+ @Override
+ public StepName name() {
+ return name;
+ }
+
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ @Override
+ public Publisher<Void> deleteUserData(Username username) {
+ return Mono.empty();
+ }
+ };
+ }
+
+ private static final String SERIALIZED_TASK = "{\"type\":\"DeleteUsersDataOfDomainTask\",\"domain\":\"domain\"}";
+ private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\":\"DeleteUsersDataOfDomainTask\",\"domain\":\"domain\",\"successfulUsersCount\":99,\"failedUsersCount\":1,\"timestamp\":\"2018-11-13T12:00:55Z\"}";
+
+ private static final DeleteUserDataService SERVICE = new DeleteUserDataService(ImmutableSet.of(A, B, C, D));
+
+ @Test
+ void taskShouldBeSerializable() throws Exception {
+ UsersRepository usersRepository = mock(UsersRepository.class);
+ JsonSerializationVerifier.dtoModule(DeleteUsersDataOfDomainTaskDTO.module(SERVICE, usersRepository))
+ .bean(new DeleteUsersDataOfDomainTask(SERVICE, DOMAIN, usersRepository))
+ .json(SERIALIZED_TASK)
+ .verify();
+ }
+
+ @Test
+ void additionalInformationShouldBeSerializable() throws Exception {
+ JsonSerializationVerifier.dtoModule(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())
+ .bean(new DeleteUsersDataOfDomainTask.AdditionalInformation(
+ TIMESTAMP, DOMAIN, SUCCESSFUL_USERS_COUNT, FAILED_USERS_COUNT))
+ .json(SERIALIZED_ADDITIONAL_INFORMATION)
+ .verify();
+ }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java
new file mode 100644
index 0000000000..f9bc9cecea
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java
@@ -0,0 +1,194 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.webadmin.service;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.util.Set;
+
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.task.Task;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+
+import reactor.core.publisher.Mono;
+
+class DeleteUsersDataOfDomainTaskTest {
+ public static class FailureStepUponUser implements DeleteUserDataTaskStep {
+ private final Set<Username> usersToBeFailed;
+
+ public FailureStepUponUser(Set<Username> usersToBeFailed) {
+ this.usersToBeFailed = usersToBeFailed;
+ }
+
+ @Override
+ public StepName name() {
+ return new StepName("FailureStepUponUser");
+ }
+
+ @Override
+ public int priority() {
+ return 0;
+ }
+
+ @Override
+ public Publisher<Void> deleteUserData(Username username) {
+ if (usersToBeFailed.contains(username)) {
+ return Mono.error(new RuntimeException());
+ }
+ return Mono.empty();
+ }
+ }
+
+ private static final Domain DOMAIN_1 = Domain.of("domain1.tld");
+ private static final Domain DOMAIN_2 = Domain.of("domain2.tld");
+
+ private DeleteUserDataService service;
+ private MemoryUsersRepository usersRepository;
+
+ @BeforeEach
+ void setup() throws Exception {
+ DNSService dnsService = mock(DNSService.class);
+ when(dnsService.getHostName(any())).thenReturn("localhost");
+ when(dnsService.getLocalHost()).thenReturn(InetAddress.getByName("localhost"));
+ MemoryDomainList domainList = new MemoryDomainList(dnsService);
+ domainList.configure(DomainListConfiguration.builder()
+ .autoDetect(false)
+ .autoDetectIp(false)
+ .build());
+ domainList.addDomain(DOMAIN_1);
+ domainList.addDomain(DOMAIN_2);
+
+ usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+ }
+
+ @Test
+ void shouldCountSuccessfulUsers() throws UsersRepositoryException {
+ // GIVEN DOMAIN1 has 2 users
+ usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+
+ // WHEN run task for DOMAIN1
+ service = new DeleteUserDataService(Set.of());
+ DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+ Task.Result result = task.run();
+
+ // THEN should count successful DOMAIN1 users
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+ softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(0L);
+ });
+ }
+
+ @Test
+ void shouldCountOnlySuccessfulUsersOfRequestedDomain() throws UsersRepositoryException {
+ // GIVEN DOMAIN1 has 2 users and DOMAIN2 has 1 user
+ usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user3@domain2.tld"), "password");
+
+ // WHEN run task for DOMAIN1
+ service = new DeleteUserDataService(Set.of());
+ DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+ Task.Result result = task.run();
+
+ // THEN should count only successful DOMAIN1 users
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(result).isEqualTo(Task.Result.COMPLETED);
+ softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+ softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(0L);
+ });
+ }
+
+ @Test
+ void shouldCountFailedUsers() throws UsersRepositoryException {
+ // GIVEN DOMAIN1 has 2 users
+ usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+
+ // WHEN run task for DOMAIN1
+ Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"), Username.of("user2@domain1.tld"));
+ service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+ DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+ Task.Result result = task.run();
+
+ // THEN should count failed DOMAIN1 users
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+ softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(0L);
+ softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(2L);
+ });
+ }
+
+ @Test
+ void shouldCountOnlyFailedUsersOfRequestedDomain() throws UsersRepositoryException {
+ // GIVEN DOMAIN1 has 2 users and DOMAIN2 has 1 user
+ usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user3@domain2.tld"), "password");
+
+ // WHEN run task for DOMAIN1
+ Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"), Username.of("user2@domain1.tld"), Username.of("user3@domain2.tld"));
+ service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+ DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+ Task.Result result = task.run();
+
+ // THEN should count only failed DOMAIN1 users
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+ softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(0L);
+ softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(2L);
+ });
+ }
+
+ @Test
+ void mixedSuccessfulAndFailedUsersCase() throws UsersRepositoryException {
+ // GIVEN DOMAIN1 has 3 users
+ usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+ usersRepository.addUser(Username.of("user3@domain1.tld"), "password");
+
+ // WHEN run task for DOMAIN1
+ Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"));
+ service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+ DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+ Task.Result result = task.run();
+
+ // THEN should count both successful and failed users
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+ softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+ softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(1L);
+ });
+ }
+}
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 329cd488ad..5864d0a644 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -178,6 +178,7 @@ Response codes:
- [Get the list of aliases for a domain](#Get_the_list_of_aliases_for_a_domain)
- [Create an alias for a domain](#Create_an_alias_for_a_domain)
- [Delete an alias for a domain](#Delete_an_alias_for_a_domain)
+ - [Delete all users data of a domain](#delete-all-users-data-of-a-domain)
### Create a domain
@@ -302,6 +303,33 @@ Response codes:
- 400: source, domain and destination domain are the same
- 404: `source.domain.tld` are not part of handled domains.
+### Delete all users data of a domain
+
+```
+curl -XPOST http://ip:port/domains/{domainToBeUsed}?action=deleteData
+```
+
+Would create a task that deletes data of all users of the domain.
+
+[More details about endpoints returning a task](#_endpoints_returning_a_task).
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Error in the request. Details can be found in the reported error.
+
+The scheduled task will have the following type `DeleteUsersDataOfDomainTask` and the following `additionalInformation`:
+
+```
+{
+ "type": "DeleteUsersDataOfDomainTask",
+ "domain": "domain.tld",
+ "successfulUsersCount": 2,
+ "failedUsersCount": 1,
+ "timestamp": "2023-05-22T08:52:47.076261Z"
+}
+```
+
## Administrating users
- [Create a user](#Create_a_user)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org