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 249f67596d6d04fcf77a69daa975694968f55977 Author: Benoit TELLIER <btell...@linagora.com> AuthorDate: Tue Jun 3 22:34:46 2025 +0200 JAMES-4316 Routes for getting and deleted mapping sources --- .../modules/servers/partials/operate/webadmin.adoc | 44 ++++++- .../james/webadmin/routes/MappingRoutes.java | 39 ++++++ .../james/webadmin/routes/MappingRoutesTest.java | 133 +++++++++++++++++++++ src/site/markdown/server/manage-webadmin.md | 42 +++++++ 4 files changed, 257 insertions(+), 1 deletion(-) diff --git a/docs/modules/servers/partials/operate/webadmin.adoc b/docs/modules/servers/partials/operate/webadmin.adoc index 8fe440c42c..bea121ca14 100644 --- a/docs/modules/servers/partials/operate/webadmin.adoc +++ b/docs/modules/servers/partials/operate/webadmin.adoc @@ -3482,7 +3482,7 @@ Response code: === Listing User Mappings -This endpoint allows receiving all mappings of a corresponding user. +This endpoint allows receiving all mappings originating from a corresponding user. .... curl -XGET http://ip:port/mappings/user/{userAddress} @@ -3516,6 +3516,48 @@ Response codes: * 200: OK * 400: Invalid parameter value +=== Listing sources for a mapping + +This endpoint allows receiving all mappings pointing to a corresponding user. + +.... +curl -XGET http://ip:port/mappings/sources/{userAddress}?type={type} +.... + +Return all mappings of a user where: + +* `userAddress`: is the selected user +* `type`: Type of the mapping. One of `group`, `forward`, `address`, `alias`. Compulsory. + +Response body: + +.... +["gro...@domain.tld","gro...@domain.tld"] +.... + +Response codes: + +* 200: OK +* 400: Invalid parameter value + +=== Deleting sources for a mapping + +This endpoint allows deleting all mappings pointing to a corresponding user. + +.... +curl -XDELETE http://ip:port/mappings/sources/{userAddress}?type={type} +.... + +Deletes all mappings of a user where: + +* `userAddress`: is the selected user +* `type`: Type of the mapping. One of `group`, `forward`, `address`, `alias`. Compulsory. + +Response codes: + +* 204: OK +* 400: Invalid parameter value + == Administrating mail repositories === Create a mail repository diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MappingRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MappingRoutes.java index 8e33fe9018..1d7244bd8d 100644 --- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MappingRoutes.java +++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/MappingRoutes.java @@ -19,7 +19,12 @@ package org.apache.james.webadmin.routes; +import static spark.Spark.halt; + +import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Optional; import jakarta.inject.Inject; @@ -39,7 +44,9 @@ import org.apache.james.webadmin.utils.Responses; import org.eclipse.jetty.http.HttpStatus; import com.fasterxml.jackson.core.type.TypeReference; +import com.github.fge.lambdas.Throwing; +import spark.HaltException; import spark.Request; import spark.Response; import spark.Service; @@ -48,7 +55,9 @@ public class MappingRoutes implements Routes { static final String BASE_PATH = "/mappings"; static final String USER_MAPPING_PATH = "/mappings/user/"; + static final String SOURCE_MAPPING_PATH = "/mappings/sources/"; static final String USER = "user"; + static final String SOURCE = "source"; private final JsonTransformer jsonTransformer; private final JsonExtractor<Map<MappingSource, Mappings>> jsonExtractor; @@ -72,6 +81,8 @@ public class MappingRoutes implements Routes { service.get(BASE_PATH, this::getMappings, jsonTransformer); service.put(BASE_PATH, this::addMappings); service.get(USER_MAPPING_PATH + ":" + USER, this::getUserMappings, jsonTransformer); + service.get(SOURCE_MAPPING_PATH + ":" + SOURCE, this::getMappingSources, jsonTransformer); + service.delete(SOURCE_MAPPING_PATH + ":" + SOURCE, this::deleteMappingSources, jsonTransformer); } private Map<MappingSource, Mappings> getMappings(Request request, Response response) { @@ -116,4 +127,32 @@ public class MappingRoutes implements Routes { Username username = Username.of(request.params(USER).toLowerCase()); return recipientRewriteTable.getStoredMappings(MappingSource.fromUser(username)); } + + private List<String> getMappingSources(Request request, Response response) { + Mapping mapping = Mapping.of(extractType(request), request.params(SOURCE)); + + return recipientRewriteTable.listSourcesReactive(mapping) + .map(MappingSource::asMailAddressString) + .collectList() + .block(); + } + + private HaltException deleteMappingSources(Request request, Response response) { + Mapping mapping = Mapping.of(extractType(request), request.params(SOURCE)); + + recipientRewriteTable.listSourcesReactive(mapping) + .collectList() + .block() + .forEach(Throwing.consumer(source -> recipientRewriteTable.removeMapping(source, mapping))); + + return halt(HttpStatus.NO_CONTENT_204); + } + + private static Mapping.Type extractType(Request request) { + return Optional.ofNullable(request.queryParams("type")).map(name -> Arrays.stream(Mapping.Type.values()) + .filter(v -> v.name().equalsIgnoreCase(name)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Invalid 'type' query parameter: " + name))) + .orElseThrow(() -> new IllegalArgumentException("On reversed resolution 'type' is compulsory")); + } } diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/MappingRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/MappingRoutesTest.java index 9e01a1b773..0f8ec6ace7 100644 --- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/MappingRoutesTest.java +++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/MappingRoutesTest.java @@ -282,6 +282,139 @@ class MappingRoutesTest { "}"); } + @Test + void getSourcesShouldReturnGroupMappings() throws Exception { + MailAddress groupAddress = new MailAddress("gr...@domain.tld"); + MailAddress group2Address = new MailAddress("gro...@domain.tld"); + MailAddress group3Address = new MailAddress("gro...@domain.tld"); + + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(groupAddress), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(group2Address), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(groupAddress), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(group3Address), "memb...@domain.tld"); + + String jsonBody = given() + .queryParam("type", "group") + .when() + .get("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.OK_200) + .extract() + .body() + .asString(); + + assertThatJson(jsonBody) + .when(Option.IGNORING_ARRAY_ORDER) + .isEqualTo(""" + ["gr...@domain.tld","gro...@domain.tld"]"""); + } + + @Test + void deleteShouldBeIdempotent() { + given() + .queryParam("type", "group") + .when() + .delete("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.NO_CONTENT_204); + } + + @Test + void shouldNotReturnDeletedSources() throws Exception { + MailAddress groupAddress = new MailAddress("gr...@domain.tld"); + MailAddress group2Address = new MailAddress("gro...@domain.tld"); + MailAddress group3Address = new MailAddress("gro...@domain.tld"); + + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(groupAddress), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(group2Address), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(groupAddress), "memb...@domain.tld"); + recipientRewriteTable.addGroupMapping( + MappingSource.fromMailAddress(group3Address), "memb...@domain.tld"); + + given() + .queryParam("type", "group") + .when() + .delete("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.NO_CONTENT_204); + + String jsonBody = when() + .get() + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.OK_200) + .extract() + .body() + .asString(); + + assertThatJson(jsonBody) + .when(Option.IGNORING_ARRAY_ORDER) + .isEqualTo("{" + + " \"gr...@domain.tld\": [" + + " {" + + " \"type\": \"Group\"," + + " \"mapping\": \"memb...@domain.tld\"" + + " }" + + " ]," + + " \"gro...@domain.tld\": [" + + " {" + + " \"type\": \"Group\"," + + " \"mapping\": \"memb...@domain.tld\"" + + " }" + + " ]" + + "}"); + } + + @Test + void getSourcesShouldRejectInvalidType() { + given() + .queryParam("type", "invalid") + .when() + .get("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.BAD_REQUEST_400); + } + + @Test + void getSourcesShouldRejectNoType() { + when() + .get("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.BAD_REQUEST_400); + } + + @Test + void deleteSourcesShouldRejectInvalidType() { + given() + .queryParam("type", "invalid") + .when() + .delete("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.BAD_REQUEST_400); + } + + @Test + void deleteSourcesShouldRejectNoType() { + when() + .delete("/sources/memb...@domain.tld") + .then() + .contentType(ContentType.JSON) + .statusCode(HttpStatus.BAD_REQUEST_400); + } + @Test void getMappingsShouldReturnForwardMappings() throws RecipientRewriteTableException { Username forwardUsername = Username.of("forwardu...@domain.tld"); diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md index 5a82d26dc6..47acb46356 100644 --- a/src/site/markdown/server/manage-webadmin.md +++ b/src/site/markdown/server/manage-webadmin.md @@ -3397,6 +3397,48 @@ Response codes: - 200: OK - 400: Invalid parameter value +### Listing sources for a mapping + +This endpoint allows receiving all mappings pointing to a corresponding user. + +``` +curl -XGET http://ip:port/mappings/sources/{userAddress}?type={type} +``` + +Return all mappings of a user where: + + - `userAddress`: is the selected user + - `type`: Type of the mapping. One of `group`, `forward`, `address`, `alias`. Compulsory. + +Response body: + +``` +["gro...@domain.tld","gro...@domain.tld"] +``` + +Response codes: + + - 200: OK + - 400: Invalid parameter value + +### Deleting sources for a mapping + +This endpoint allows deleting all mappings pointing to a corresponding user. + +``` +curl -XDELETE http://ip:port/mappings/sources/{userAddress}?type={type} +``` + +Deletes all mappings of a user where: + + - `userAddress`: is the selected user + - `type`: Type of the mapping. One of `group`, `forward`, `address`, `alias`. Compulsory. + +Response codes: + + - 204: OK + - 400: Invalid parameter value + ## Administrating mail repositories - [Create a mail repository](#Create_a_mail_repository) --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org