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/24 07:18:48 UTC

[james-project] 04/06: JAMES-3909 Webadmin route for user data deletion

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 42dee87a6d8a8f083283d9956a20b142e5463e8d
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Thu May 18 16:51:56 2023 +0700

    JAMES-3909 Webadmin route for user data deletion
    
    Co-authored-by: Benoit Tellier <bt...@linagora.com>
---
 .../webadmin/routes/DeleteUserDataRoutes.java      |  84 +++++++
 .../webadmin/routes/DeleteUserDataRoutesTest.java  | 278 +++++++++++++++++++++
 2 files changed, 362 insertions(+)

diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DeleteUserDataRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DeleteUserDataRoutes.java
new file mode 100644
index 0000000000..f581ed7127
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DeleteUserDataRoutes.java
@@ -0,0 +1,84 @@
+/****************************************************************
+ * 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.routes;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.task.TaskManager;
+import org.apache.james.user.api.DeleteUserDataTaskStep.StepName;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.service.DeleteUserDataService;
+import org.apache.james.webadmin.service.DeleteUserDataTask;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.tasks.TaskRegistrationKey;
+import org.apache.james.webadmin.utils.JsonTransformer;
+
+import com.google.common.base.Preconditions;
+
+import spark.Route;
+import spark.Service;
+
+public class DeleteUserDataRoutes implements Routes {
+    private static final String USER_PATH_PARAM = ":username";
+    private static final String ROOT_PATH = "/users/" + USER_PATH_PARAM;
+    private static final TaskRegistrationKey DELETE_USER_DATA = TaskRegistrationKey.of("deleteData");
+
+    private final UsersRepository usersRepository;
+    private final DeleteUserDataService service;
+    private final TaskManager taskManager;
+    private final JsonTransformer jsonTransformer;
+
+    @Inject
+    DeleteUserDataRoutes(UsersRepository usersRepository, DeleteUserDataService service, TaskManager taskManager, JsonTransformer jsonTransformer) {
+        this.usersRepository = usersRepository;
+        this.service = service;
+        this.taskManager = taskManager;
+        this.jsonTransformer = jsonTransformer;
+    }
+
+    @Override
+    public String getBasePath() {
+        return ROOT_PATH;
+    }
+
+    @Override
+    public void define(Service service) {
+        service.post(ROOT_PATH, deleteUserData(), jsonTransformer);
+    }
+
+    public Route deleteUserData() {
+        return TaskFromRequestRegistry.builder()
+            .parameterName("action")
+            .register(DELETE_USER_DATA, request -> {
+                Username username = Username.of(request.params(USER_PATH_PARAM));
+
+                Preconditions.checkArgument(usersRepository.contains(username), "'username' parameter should be an existing user");
+
+                Optional<StepName> fromStep = Optional.ofNullable(request.queryParams("fromStep")).map(StepName::new);
+
+                return new DeleteUserDataTask(service, username, fromStep);
+            })
+            .buildAsRoute(taskManager);
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DeleteUserDataRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DeleteUserDataRoutesTest.java
new file mode 100644
index 0000000000..f3939320e1
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DeleteUserDataRoutesTest.java
@@ -0,0 +1,278 @@
+/****************************************************************
+ * 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.routes;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.with;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+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.json.DTOConverter;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+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.DeleteUserDataTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.hamcrest.Matchers;
+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.ImmutableSet;
+
+import io.restassured.RestAssured;
+import reactor.core.publisher.Mono;
+
+class DeleteUserDataRoutesTest {
+
+    private static final Username USER = Username.of("jessy.jones@domain.tld");
+
+    public static class StepImpl implements DeleteUserDataTaskStep {
+        private final StepName name;
+        private final int priority;
+        private final Mono<Void> behaviour;
+
+        public StepImpl(StepName name, int priority, Mono<Void> behaviour) {
+            this.name = name;
+            this.priority = priority;
+            this.behaviour = behaviour;
+        }
+
+        @Override
+        public StepName name() {
+            return name;
+        }
+
+        @Override
+        public int priority() {
+            return priority;
+        }
+
+        @Override
+        public Publisher<Void> deleteUserData(Username username) {
+            return behaviour;
+        }
+    }
+
+    private MemoryUsersRepository usersRepository;
+
+    WebAdminServer setUp(ImmutableSet<DeleteUserDataTaskStep> steps) {
+        MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
+        DeleteUserDataService service = new DeleteUserDataService(steps);
+        WebAdminServer webAdminServer = WebAdminUtils
+            .createWebAdminServer(new DeleteUserDataRoutes(usersRepository, service, taskManager, new JsonTransformer()),
+                new TasksRoutes(taskManager, new JsonTransformer(), DTOConverter.of(DeleteUserDataTaskAdditionalInformationDTO.module())))
+            .start();
+
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .build();
+
+        return webAdminServer;
+    }
+
+    @BeforeEach
+    void setUpUsersRepo() throws Exception {
+        DNSService dnsService = mock(DNSService.class);
+        MemoryDomainList domainList = new MemoryDomainList(dnsService);
+        domainList.configure(DomainListConfiguration.DEFAULT);
+        domainList.addDomain(Domain.of("domain.tld"));
+        usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+    }
+
+    @Nested
+    class BasicTests {
+        private WebAdminServer webAdminServer;
+        private AtomicBoolean behaviour1;
+        private AtomicBoolean behaviour2;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            behaviour1 = new AtomicBoolean(false);
+            behaviour2 = new AtomicBoolean(false);
+            webAdminServer = DeleteUserDataRoutesTest.this.setUp(
+                ImmutableSet.of(new StepImpl(new DeleteUserDataTaskStep.StepName("A"), 35, Mono.fromRunnable(() -> behaviour1.set(true))),
+                    new StepImpl(new DeleteUserDataTaskStep.StepName("B"), 3, Mono.fromRunnable(() -> behaviour2.set(true)))));
+
+            usersRepository.addUser(USER, "pass");
+        }
+
+        @AfterEach
+        void stop() {
+            webAdminServer.destroy();
+        }
+
+        @Test
+        void shouldPerformDataDeletion() {
+            String taskId = with()
+                .queryParam("action", "deleteData")
+                .post("/users/" + USER.asString())
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("type", is("DeleteUserDataTask"))
+                .body("status", is("completed"))
+                .body("additionalInformation.type", is("DeleteUserDataTask"))
+                .body("additionalInformation.username", is("jessy.jones@domain.tld"))
+                .body("additionalInformation.status.A", is("DONE"))
+                .body("additionalInformation.status.B", is("DONE"));
+
+            assertThat(behaviour1.get()).isTrue();
+            assertThat(behaviour2.get()).isTrue();
+        }
+
+        @Test
+        void shouldFailWhenInvalidAction() {
+            given()
+                .queryParam("action", "invalid")
+            .post("/users/" + USER.asString())
+            .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 shouldRejectUnknownUser() {
+            given()
+                .queryParam("action", "deleteData")
+            .when()
+                .post("/users/unknown@domain.tld")
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", Matchers.is(400))
+                .body("type", Matchers.is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", Matchers.is("Invalid arguments supplied in the user request"))
+                .body("details", Matchers.is("'username' parameter should be an existing user"));
+        }
+    }
+
+    @Nested
+    class ResumeFailureTests {
+        private WebAdminServer webAdminServer;
+        private AtomicBoolean behaviour1;
+        private AtomicBoolean behaviour2;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            behaviour1 = new AtomicBoolean(false);
+            behaviour2 = new AtomicBoolean(false);
+            webAdminServer = DeleteUserDataRoutesTest.this.setUp(
+                ImmutableSet.of(new StepImpl(new DeleteUserDataTaskStep.StepName("A"), 1, Mono.fromRunnable(() -> behaviour1.set(true))),
+                    new StepImpl(new DeleteUserDataTaskStep.StepName("B"), 2, Mono.error(RuntimeException::new)),
+                    new StepImpl(new DeleteUserDataTaskStep.StepName("C"), 3, Mono.fromRunnable(() -> behaviour2.set(true)))));
+
+            usersRepository.addUser(USER, "pass");
+        }
+
+        @AfterEach
+        void stop() {
+            webAdminServer.destroy();
+        }
+
+        @Test
+        void shouldReportFailures() {
+            String taskId = with()
+                .queryParam("action", "deleteData")
+                .post("/users/" + USER.asString())
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("type", is("DeleteUserDataTask"))
+                .body("status", is("failed"))
+                .body("additionalInformation.type", is("DeleteUserDataTask"))
+                .body("additionalInformation.username", is("jessy.jones@domain.tld"))
+                .body("additionalInformation.status.A", is("DONE"))
+                .body("additionalInformation.status.B", is("FAILED"))
+                .body("additionalInformation.status.C", is("ABORTED"));
+
+            assertThat(behaviour1.get()).isTrue();
+            assertThat(behaviour2.get()).isFalse();
+        }
+
+        @Test
+        void shouldSupportResumeWhenFailure() {
+            String taskId = with()
+                .queryParam("action", "deleteData")
+                .queryParam("fromStep", "B")
+                .post("/users/" + USER.asString())
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("type", is("DeleteUserDataTask"))
+                .body("status", is("failed"))
+                .body("additionalInformation.type", is("DeleteUserDataTask"))
+                .body("additionalInformation.username", is("jessy.jones@domain.tld"))
+                .body("additionalInformation.status.A", is("SKIPPED"))
+                .body("additionalInformation.status.B", is("FAILED"))
+                .body("additionalInformation.status.C", is("ABORTED"));
+
+            assertThat(behaviour1.get()).isFalse();
+            assertThat(behaviour2.get()).isFalse();
+        }
+
+        @Test
+        void shouldRejectInvalidFromStep() {
+            given()
+                .queryParam("action", "deleteData")
+                .queryParam("fromStep", "invalid")
+            .when()
+                .post("/users/" + USER.asString())
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", Matchers.is(400))
+                .body("type", Matchers.is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", Matchers.is("Invalid arguments supplied in the user request"))
+                .body("details", Matchers.is("Starting step not found: invalid"));
+        }
+    }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org