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 2024/06/06 13:20:43 UTC
(james-project) 05/06: JAMES-3946 add WebAdmin API to manage the DropList (#2241)
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch droplist
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c3210c21888bff0840b158f922f46cb92d87354a
Author: Maksim <85...@users.noreply.github.com>
AuthorDate: Tue May 14 10:29:52 2024 +0300
JAMES-3946 add WebAdmin API to manage the DropList (#2241)
---
.../docs/modules/ROOT/pages/operate/webadmin.adoc | 120 +++++++++
.../james/webadmin/routes/DropListRoutes.java | 218 ++++++++++++++++
.../james/webadmin/routes/DropListRoutesTest.java | 273 +++++++++++++++++++++
3 files changed, 611 insertions(+)
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 81ab4f0c00..0186ff42b0 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
@@ -2835,6 +2835,126 @@ Response codes:
* 204: Operation succeeded
+== Administrating DropLists
+
+The DropList, also known as the mail blacklist, is a collection of
+domains and email addresses that are denied from sending emails within the system.
+
+Owner scopes:
+
+- *global*: contains entries that are blocked across all domains and addresses within the system.
+Entries in the global owner scope apply universally and affect all users and domains.
+- *domain*: each domain can have its own droplist, which contains entries specific to that domain.
+- *user*: allow to customize personalized droplist of blocked domains and email addresses.
+
+The `deniedEntityType` query parameter is optional and can take the values `domain` or `address`.
+
+=== Getting the DropList
+==== Global DropList
+....
+curl -XGET http://ip:port/droplist/global?deniedEntityType=null|domain|address
+....
+==== Domain DropList
+....
+curl -XGET http://ip:port/droplist/domain/target.com?deniedEntityType=null|domain|address
+....
+==== User DropList
+....
+curl -XGET http://ip:port/droplist/user/tagret@target.com?deniedEntityType=null|domain|address
+....
+
+The answer looks like:
+....
+[ "evil.com", "devil.com", "bad_guy@crime.com", "hacker@murder.org" ]
+....
+
+Response codes:
+
+* 200: The drop list was successfully retrieved
+* 400: Invalid `owner scope` or `deniedEntityType`
+
+=== Testing a denied entity existence
+==== Global DropList
+....
+curl -XHEAD http://ip:port/droplist/global/attacker@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XHEAD http://ip:port/droplist/domain/target.com/attacker@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XHEAD http://ip:port/droplist/user/target@target.com/attacker@evil.com
+....
+....
+curl -XHEAD http://ip:port/droplist/user/target@target.com/evil.com
+....
+Response codes:
+
+* 200: The denied entity exists
+* 404: The denied entity does not exist
+
+=== Add Entry to the DropList
+The denied entity must be a valid email address or link:#_create_a_domain[domain].
+
+==== Global DropList
+....
+curl -XPUT http://ip:port/droplist/global/attacker@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XPUT http://ip:port/droplist/domain/target.com/attacker@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XPUT http://ip:port/droplist/user/target@target.com/attacker@evil.com
+....
+....
+curl -XPUT http://ip:port/droplist/user/target@target.com/evil.com
+....
+Response codes:
+
+* 204: The denied entity was successfully added
+* 400: The denied entity is invalid
+
+=== Remove Entry from the DropList
+==== Global DropList
+....
+curl -XDELETE http://ip:port/droplist/global/attacker@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/global/evil.com
+....
+==== Domain DropList
+....
+curl -XDELETE http://ip:port/droplist/domain/target.com/attacker@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/domain/target.com/evil.com
+....
+==== User DropList
+....
+curl -XDELETE http://ip:port/droplist/user/target@target.com/attacker@evil.com
+....
+....
+curl -XDELETE http://ip:port/droplist/user/target@target.com/evil.com
+....
+Response codes:
+
+* 204: Entry deleted successfully.
+
== Administrating Jmap Uploads
=== Cleaning upload repository
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java
new file mode 100644
index 0000000000..5fdb93f28e
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DropListRoutes.java
@@ -0,0 +1,218 @@
+/****************************************************************
+ * 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 org.apache.james.webadmin.Constants.SEPARATOR;
+
+import java.util.List;
+import java.util.Optional;
+
+import jakarta.inject.Inject;
+import jakarta.mail.internet.AddressException;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.james.core.Domain;
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DeniedEntityType;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+import org.apache.james.util.ReactorUtils;
+import org.apache.james.webadmin.Constants;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.Responses;
+import org.eclipse.jetty.http.HttpStatus;
+
+import com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Flux;
+import spark.Request;
+import spark.Response;
+import spark.Service;
+
+public class DropListRoutes implements Routes {
+ public static final String DROP_LIST = "/droplist";
+ public static final String OWNER_SCOPE = ":ownerScope";
+ public static final String OWNER = ":owner";
+ public static final String DENIED_ENTITY = ":deniedEntity";
+ public static final String DENIED_ENTITY_TYPE = "deniedEntityType";
+
+ private final DropList dropList;
+ private final JsonTransformer jsonTransformer;
+
+ @Inject
+ public DropListRoutes(DropList dropList, JsonTransformer jsonTransformer) {
+ this.dropList = dropList;
+ this.jsonTransformer = jsonTransformer;
+ }
+
+ @Override
+ public String getBasePath() {
+ return DROP_LIST;
+ }
+
+ @Override
+ public void define(Service service) {
+ service.get(DROP_LIST + SEPARATOR + OWNER_SCOPE, this::getDropList, jsonTransformer);
+ service.get(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER, this::getDropList, jsonTransformer);
+ service.put(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER + SEPARATOR + DENIED_ENTITY, this::addDropListEntry);
+ service.put(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + DENIED_ENTITY, this::addDropListEntry);
+ service.head(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER + SEPARATOR + DENIED_ENTITY, this::dropListEntryExist);
+ service.head(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + DENIED_ENTITY, this::dropListEntryExist);
+ service.delete(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + OWNER + SEPARATOR + DENIED_ENTITY, this::removeDropListEntry);
+ service.delete(DROP_LIST + SEPARATOR + OWNER_SCOPE + SEPARATOR + DENIED_ENTITY, this::removeDropListEntry);
+ }
+
+ public ImmutableSet<String> getDropList(Request request, Response response) {
+ OwnerScope ownerScope = checkValidOwnerScope(request.params(OWNER_SCOPE));
+ String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+ Optional<DeniedEntityType> deniedEntityType = checkValidDeniedEntityType(request.queryParams(DENIED_ENTITY_TYPE));
+ if (deniedEntityType.isPresent()) {
+ return dropList.list(ownerScope, owner)
+ .filter(deniedEntry -> deniedEntry.getDeniedEntityType().equals(deniedEntityType.get()))
+ .map(DropListEntry::getDeniedEntity)
+ .collect(ImmutableSet.toImmutableSet())
+ .block();
+ } else {
+ return dropList.list(ownerScope, owner)
+ .map(DropListEntry::getDeniedEntity)
+ .collect(ImmutableSet.toImmutableSet())
+ .block();
+ }
+ }
+
+ public String addDropListEntry(Request request, Response response) {
+ OwnerScope ownerScope = checkValidOwnerScope(request.params(OWNER_SCOPE));
+ String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+ String deniedEntity = request.params(DENIED_ENTITY);
+ DropListEntry dropListEntry = getDropListEntry(ownerScope, owner, deniedEntity);
+ dropList.add(dropListEntry).block();
+ return Responses.returnNoContent(response);
+ }
+
+ public String removeDropListEntry(Request request, Response response) {
+ OwnerScope ownerScope = checkValidOwnerScope(request.params(OWNER_SCOPE));
+ String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+ String deniedEntity = request.params(DENIED_ENTITY);
+ dropList.list(ownerScope, owner)
+ .filter(dropListEntry -> dropListEntry.getDeniedEntity().equals(deniedEntity))
+ .collectList()
+ .doOnNext(this::deleteDropListEntry)
+ .block();
+ return Responses.returnNoContent(response);
+ }
+
+ public String dropListEntryExist(Request request, Response response) {
+ OwnerScope ownerScope = checkValidOwnerScope(request.params(OWNER_SCOPE));
+ String owner = Optional.ofNullable(request.params(OWNER)).orElse("");
+ String deniedEntity = request.params(DENIED_ENTITY);
+ boolean entryExists = dropList.list(ownerScope, owner)
+ .any(dropListEntry -> dropListEntry.getDeniedEntity().equals(deniedEntity))
+ .block();
+ if (entryExists) {
+ response.status(HttpStatus.NO_CONTENT_204);
+ } else {
+ response.status(HttpStatus.NOT_FOUND_404);
+ }
+ return Constants.EMPTY_BODY;
+ }
+
+ private Optional<DeniedEntityType> checkValidDeniedEntityType(String deniedEntityType) {
+ try {
+ if (deniedEntityType == null || deniedEntityType.isEmpty()) {
+ return Optional.empty();
+ } else {
+ return Optional.of(DeniedEntityType.valueOf(deniedEntityType.toUpperCase()));
+ }
+ } catch (IllegalArgumentException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+ .message("Invalid DeniedEntityType")
+ .cause(new IllegalArgumentException("DeniedEntityType '" + deniedEntityType + "' is invalid. Supported values are " +
+ EnumUtils.getEnumList(DeniedEntityType.class)))
+ .haltError();
+ }
+ }
+
+ private DropListEntry getDropListEntry(OwnerScope ownerScope, String owner, String deniedEntity) {
+ DropListEntry.Builder dropListEntryBuilder = DropListEntry.builder();
+ switch (ownerScope) {
+ case GLOBAL -> dropListEntryBuilder = dropListEntryBuilder.forAll();
+ case DOMAIN -> dropListEntryBuilder = dropListEntryBuilder.domainOwner(checkValidDomain(owner));
+ case USER -> dropListEntryBuilder = dropListEntryBuilder.userOwner(checkValidMailAddress(owner));
+ }
+ if (deniedEntity.contains("@")) {
+ dropListEntryBuilder.denyAddress(checkValidMailAddress(deniedEntity));
+ } else {
+ dropListEntryBuilder.denyDomain(checkValidDomain(deniedEntity));
+ }
+ return dropListEntryBuilder.build();
+ }
+
+ private OwnerScope checkValidOwnerScope(String ownerScope) {
+ try {
+ return OwnerScope.valueOf(ownerScope.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+ .message("Invalid OwnerScope")
+ .cause(new IllegalArgumentException("OwnerScope '" + ownerScope + "' is invalid. Supported values are " +
+ EnumUtils.getEnumList(OwnerScope.class)))
+ .haltError();
+ }
+ }
+
+ private static MailAddress checkValidMailAddress(String address) {
+ try {
+ return new MailAddress(address);
+ } catch (AddressException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+ .message("Invalid mail address %s", address)
+ .cause(e)
+ .haltError();
+ }
+ }
+
+ private Domain checkValidDomain(String domainName) {
+ try {
+ return Domain.of(domainName);
+ } catch (IllegalArgumentException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+ .message("Invalid domain %s", domainName)
+ .cause(e)
+ .haltError();
+ }
+ }
+
+ private void deleteDropListEntry(List<DropListEntry> dropListEntries) {
+ Flux.fromIterable(dropListEntries)
+ .flatMap(dropList::remove, ReactorUtils.DEFAULT_CONCURRENCY)
+ .then()
+ .block();
+ }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java
new file mode 100644
index 0000000000..3ba3df5d2e
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DropListRoutesTest.java
@@ -0,0 +1,273 @@
+/****************************************************************
+ * 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.when;
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.is;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import jakarta.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.droplists.api.DropList;
+import org.apache.james.droplists.api.DropListEntry;
+import org.apache.james.droplists.api.OwnerScope;
+import org.apache.james.droplists.memory.MemoryDropList;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+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.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+
+class DropListRoutesTest {
+ private static final String DENIED_SENDER = "attacker@evil.com";
+ private static final String OWNER_RECIPIENT = "owner@owner.com";
+
+ private WebAdminServer webAdminServer;
+ private DropList dropList;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ dropList = new MemoryDropList();
+ DropListRoutes dropListRoutes = new DropListRoutes(dropList, new JsonTransformer());
+ this.webAdminServer = WebAdminUtils.createWebAdminServer(dropListRoutes).start();
+ RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer).build();
+ RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
+ getDropListTestEntries().forEach(entry -> dropList.add(entry).block());
+ }
+
+ @AfterEach
+ void tearDown() throws AddressException {
+ webAdminServer.destroy();
+ getDropListTestEntries().forEach(entry -> dropList.remove(entry).block());
+ }
+
+ @ParameterizedTest(name = "{index} Owner: {0}")
+ @ValueSource(strings = {
+ "global",
+ "domain/owner.com",
+ "user/owner@owner.com"})
+ void shouldGetFullDropList(String pathParam) {
+ when()
+ .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+ .then()
+ .statusCode(HttpStatus.OK_200)
+ .contentType(ContentType.JSON)
+ .body(".", containsInAnyOrder("attacker@evil.com", "evil.com"));
+ }
+
+ @ParameterizedTest(name = "{index} Owner: {0}")
+ @ValueSource(strings = {
+ "unknown",
+ "unknown/owner.com",
+ "unknown/owner@owner.com"})
+ void shouldHandleWhenGetDropListWithInvalidOwnerScope(String pathParam) {
+ when()
+ .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+ .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+ .body("message", is("Invalid OwnerScope"))
+ .body("details", is("OwnerScope 'unknown' is invalid. Supported values are [GLOBAL, DOMAIN, USER]"));
+ }
+
+ @ParameterizedTest(name = "{index} Owner: {0}, DeniedEntityType: {1}")
+ @CsvSource(value = {
+ "global, domain, evil.com",
+ "global, address, attacker@evil.com",
+ "domain/owner.com, domain, evil.com",
+ "domain/owner.com, address, attacker@evil.com",
+ "user/owner@owner.com, domain, evil.com",
+ "user/owner@owner.com, address, attacker@evil.com"})
+ void shouldGetDropListWithQueryParams(String pathParam, String queryParam, String expected) {
+ given()
+ .queryParam("deniedEntityType", queryParam)
+ .when()
+ .get(DropListRoutes.DROP_LIST + SEPARATOR + pathParam)
+ .then()
+ .statusCode(HttpStatus.OK_200)
+ .contentType(ContentType.JSON)
+ .body(".", containsInAnyOrder(expected));
+ }
+
+ @Test
+ void shouldHandleInvalidDeniedEntityType() {
+ given()
+ .queryParam("deniedEntityType", "unknown")
+ .when()
+ .get(DropListRoutes.DROP_LIST + SEPARATOR + "global")
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+ .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+ .body("message", is("Invalid DeniedEntityType"))
+ .body("details", is("DeniedEntityType 'unknown' is invalid. Supported values are [ADDRESS, DOMAIN]"));
+ }
+
+ @ParameterizedTest(name = "{index} OwnerScope: {0}, Owner: {1}, DeniedEntity: {2}")
+ @CsvSource(value = {
+ "global, , devil.com",
+ "global, , bad_guy@crime.com",
+ "domain, owner.com, devil.com",
+ "domain, owner.com, bad_guy@crime.com",
+ "user, owner@owner.com, devil.com",
+ "user, owner@owner.com, bad_guy@crime.com"})
+ void shouldAddDropListEntry(String ownerScope, String owner, String newDeniedEntity) {
+ when()
+ .put(DropListRoutes.DROP_LIST + SEPARATOR + ownerScope + SEPARATOR + owner + SEPARATOR + newDeniedEntity)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT_204);
+
+ assertThat(getResultDropList(ownerScope, owner)).contains(newDeniedEntity);
+ }
+
+ @ParameterizedTest(name = "{index} OwnerScope: {0}, Owner: {1}, DeniedEntity: {2}")
+ @CsvSource(value = {
+ "global, , devil..com, Invalid domain devil..com",
+ "global, , bad_guy@@crime.com, Invalid mail address bad_guy@@crime.com",
+ "domain, owner.com, devil..com, Invalid domain devil..com",
+ "domain, owner.com, bad_guy@@crime.com, Invalid mail address bad_guy@@crime.com",
+ "user, owner@owner.com, devil..com, Invalid domain devil..com",
+ "user, owner@owner.com, bad_guy@@crime.com, Invalid mail address bad_guy@@crime.com"})
+ void shouldFailWhenAddInvalidDeniedEntity(String ownerScope, String owner, String newDeniedEntity, String message) {
+ when()
+ .put(DropListRoutes.DROP_LIST + SEPARATOR + ownerScope + SEPARATOR + owner + SEPARATOR + newDeniedEntity)
+ .then()
+ .statusCode(HttpStatus.BAD_REQUEST_400)
+ .contentType(ContentType.JSON)
+ .body("statusCode", is(HttpStatus.BAD_REQUEST_400))
+ .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+ .body("message", is(message));
+ }
+
+ @ParameterizedTest(name = "{index} Path: {0}")
+ @CsvSource(value = {
+ "/global/evil.com",
+ "/global/attacker@evil.com",
+ "/domain/owner.com/evil.com",
+ "/domain/owner.com/attacker@evil.com",
+ "/user/owner@owner.com/evil.com",
+ "/user/owner@owner.com/attacker@evil.com"})
+ void headShouldReturnNoContentWhenDomainDeniedEntityExists(String path) {
+ when()
+ .head(DropListRoutes.DROP_LIST + path)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT_204);
+ }
+
+ @ParameterizedTest(name = "{index} Path: {0}")
+ @CsvSource(value = {
+ "global/devil.com",
+ "global/bad_guy@crime.com",
+ "/domain/owner.com/devil.com",
+ "/domain/owner.com/bad_guy@crime.com",
+ "/user/owner@owner.com/devil.com",
+ "/user/owner@owner.com/bad_guy@crime.com"})
+ void headShouldReturnNotFoundWhenDomainDeniedEntityNotExists(String path) {
+ when()
+ .head(DropListRoutes.DROP_LIST + path)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404);
+ }
+
+ @ParameterizedTest(name = "{index} Path: {3}")
+ @CsvSource(value = {
+ "global, , evil.com, /global/evil.com",
+ "global, , attacker@evil.com, /global/attacker@evil.com",
+ "domain, owner.com, evil.com, /domain/owner.com/evil.com",
+ "domain, owner.com, attacker@evil.com, /domain/owner.com/attacker@evil.com",
+ "user, owner@owner.com, evil.com, /user/owner@owner.com/evil.com",
+ "user, owner@owner.com, attacker@evil.com, /user/owner@owner.com/attacker@evil.com"})
+ void deleteShouldReturnNoContent(String ownerScope, String owner, String deniedEntity, String path) {
+ given()
+ .delete(DropListRoutes.DROP_LIST + path)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT_204);
+
+ assertThat(getResultDropList(ownerScope, owner)).doesNotContain(deniedEntity);
+ }
+
+ @Test
+ void deleteShouldReturnNotFoundWhenUsedWithEmptyEntry() {
+ given()
+ .delete(SEPARATOR)
+ .then()
+ .statusCode(HttpStatus.NOT_FOUND_404);
+ }
+
+ static Stream<DropListEntry> getDropListTestEntries() throws AddressException {
+ return Stream.of(
+ DropListEntry.builder()
+ .forAll()
+ .denyAddress(new MailAddress(DENIED_SENDER))
+ .build(),
+ DropListEntry.builder()
+ .forAll()
+ .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+ .build(),
+ DropListEntry.builder()
+ .forAll()
+ .denyAddress(new MailAddress(DENIED_SENDER))
+ .build(),
+ DropListEntry.builder()
+ .domainOwner(new MailAddress(OWNER_RECIPIENT).getDomain())
+ .denyAddress(new MailAddress(DENIED_SENDER))
+ .build(),
+ DropListEntry.builder()
+ .domainOwner(new MailAddress(OWNER_RECIPIENT).getDomain())
+ .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+ .build(),
+ DropListEntry.builder()
+ .userOwner(new MailAddress(OWNER_RECIPIENT))
+ .denyAddress(new MailAddress(DENIED_SENDER))
+ .build(),
+ DropListEntry.builder()
+ .userOwner(new MailAddress(OWNER_RECIPIENT))
+ .denyDomain(new MailAddress(DENIED_SENDER).getDomain())
+ .build());
+ }
+
+ private List<String> getResultDropList(String ownerScope, String owner) {
+ return dropList.list(OwnerScope.valueOf(ownerScope.toUpperCase()),
+ Optional.ofNullable(owner).orElse(""))
+ .map(DropListEntry::getDeniedEntity)
+ .collectList()
+ .block();
+ }
+}
\ 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