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


The following commit(s) were added to refs/heads/master by this push:
     new 27b703b  JAMES-3657 Modular entity validation for webadmin-data (#678)
27b703b is described below

commit 27b703b8f14dab03eefdc6264d96d844da3e1358
Author: Benoit TELLIER <[email protected]>
AuthorDate: Tue Oct 5 09:00:27 2021 +0700

    JAMES-3657 Modular entity validation for webadmin-data (#678)
    
    **Why?**: Adding a new entity requires to modify all the other
    entity, which is error prone, and also fastidious...
    
    These entity includes: groups, aliases, users.
    
    **How?**: Write a modular validation system that all types would use.
    Adding a new type would just require to add its validator.
    
    **Side fixes**
    
    * JAMES-3657 PUT aliases 400 -> 409 if the user is referenced by another 
entity
---
 .../pages/distributed/operate/webadmin.adoc        |  2 +-
 .../james/modules/server/DataRoutesModules.java    | 17 +++++
 .../apache/james/webadmin/routes/AliasRoutes.java  | 25 ++++---
 .../apache/james/webadmin/routes/GroupsRoutes.java | 21 ++++--
 .../apache/james/webadmin/routes/UserRoutes.java   |  7 +-
 .../service/AggregateUserEntityValidator.java      | 43 +++++++++++
 .../service/DefaultUserEntityValidator.java        | 49 +++++++++++++
 .../RecipientRewriteTableUserEntityValidator.java  | 68 ++++++++++++++++++
 .../webadmin/service/UserEntityValidator.java      | 84 ++++++++++++++++++++++
 .../apache/james/webadmin/service/UserService.java | 27 +++----
 .../james/webadmin/routes/AliasRoutesTest.java     | 41 +++++++++--
 .../james/webadmin/routes/GroupsRoutesTest.java    | 35 +++++++--
 .../james/webadmin/routes/UserRoutesTest.java      | 58 ++++++++++++---
 src/site/markdown/server/manage-webadmin.md        |  2 +-
 14 files changed, 422 insertions(+), 57 deletions(-)

diff --git a/docs/modules/servers/pages/distributed/operate/webadmin.adoc 
b/docs/modules/servers/pages/distributed/operate/webadmin.adoc
index 7eb8320..4e4c439 100644
--- a/docs/modules/servers/pages/distributed/operate/webadmin.adoc
+++ b/docs/modules/servers/pages/distributed/operate/webadmin.adoc
@@ -2593,10 +2593,10 @@ Response codes:
 
 * 204: OK
 * 400: Alias structure or member is not valid
-* 400: The alias source exists as an user already
 * 400: Source and destination can’t be the same!
 * 400: Domain in the destination or source is not managed by the
 DomainList
+* 409: The alias source exists as an user already
 * 409: The addition of the alias would lead to a loop and thus cannot be 
performed
 
 ==== Removing an alias of an 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 1014264..ef49eac 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
@@ -19,6 +19,8 @@
 
 package org.apache.james.modules.server;
 
+import java.util.Set;
+
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.MappingSourceModule;
 import org.apache.james.webadmin.mdc.RequestLogger;
@@ -32,9 +34,15 @@ import org.apache.james.webadmin.routes.MappingRoutes;
 import org.apache.james.webadmin.routes.RegexMappingRoutes;
 import org.apache.james.webadmin.routes.UserCreationRequestLogger;
 import org.apache.james.webadmin.routes.UserRoutes;
+import org.apache.james.webadmin.service.AggregateUserEntityValidator;
+import org.apache.james.webadmin.service.DefaultUserEntityValidator;
+import 
org.apache.james.webadmin.service.RecipientRewriteTableUserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator;
 import org.apache.james.webadmin.utils.JsonTransformerModule;
 
 import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
 import com.google.inject.multibindings.Multibinder;
 
 public class DataRoutesModules extends AbstractModule {
@@ -56,5 +64,14 @@ public class DataRoutesModules extends AbstractModule {
         
jsonTransformerModuleMultibinder.addBinding().to(MappingSourceModule.class);
 
         Multibinder.newSetBinder(binder(), 
RequestLogger.class).addBinding().to(UserCreationRequestLogger.class);
+
+        Multibinder.newSetBinder(binder(), 
UserEntityValidator.class).addBinding().to(DefaultUserEntityValidator.class);
+        Multibinder.newSetBinder(binder(), 
UserEntityValidator.class).addBinding().to(RecipientRewriteTableUserEntityValidator.class);
+    }
+
+    @Provides
+    @Singleton
+    UserEntityValidator userEntityValidator(Set<UserEntityValidator> 
validatorSet) {
+        return new AggregateUserEntityValidator(validatorSet);
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
index 82c96a6..9109916 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/AliasRoutes.java
@@ -20,10 +20,12 @@
 package org.apache.james.webadmin.routes;
 
 import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static 
org.apache.james.webadmin.service.UserEntityValidator.EntityType.ALIAS;
 import static spark.Spark.halt;
 
 import java.util.Comparator;
 import java.util.List;
+import java.util.Optional;
 
 import javax.inject.Inject;
 import javax.ws.rs.DELETE;
@@ -46,10 +48,11 @@ import 
org.apache.james.rrt.api.SourceDomainIsNotInDomainListException;
 import org.apache.james.rrt.lib.Mapping;
 import org.apache.james.rrt.lib.MappingSource;
 import org.apache.james.user.api.UsersRepository;
-import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.AliasSourcesResponse;
+import org.apache.james.webadmin.service.UserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator.ValidationFailure;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
@@ -86,14 +89,16 @@ public class AliasRoutes implements Routes {
     private static final String ADDRESS_TYPE = "alias";
 
     private final UsersRepository usersRepository;
+    private final UserEntityValidator userEntityValidator;
     private final DomainList domainList;
     private final JsonTransformer jsonTransformer;
     private final RecipientRewriteTable recipientRewriteTable;
 
     @Inject
     @VisibleForTesting
-    AliasRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository 
usersRepository, DomainList domainList, JsonTransformer jsonTransformer) {
+    AliasRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository 
usersRepository, UserEntityValidator userEntityValidator, DomainList 
domainList, JsonTransformer jsonTransformer) {
         this.usersRepository = usersRepository;
+        this.userEntityValidator = userEntityValidator;
         this.domainList = domainList;
         this.jsonTransformer = jsonTransformer;
         this.recipientRewriteTable = recipientRewriteTable;
@@ -143,13 +148,13 @@ public class AliasRoutes implements Routes {
     @ApiResponses(value = {
         @ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "OK"),
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = 
ALIAS_DESTINATION_ADDRESS + " or alias structure format is not valid"),
-        @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "The alias 
source exists as an user already"),
+        @ApiResponse(code = HttpStatus.CONFLICT_409, message = "The alias 
source exists as an user already"),
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Source and 
destination can't be the same!"),
         @ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Domain in 
the destination or source is not managed by the DomainList"),
         @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
             message = "Internal server error - Something went bad on the 
server side.")
     })
-    public HaltException addAlias(Request request, Response response) throws 
UsersRepositoryException, RecipientRewriteTableException, DomainListException {
+    public HaltException addAlias(Request request, Response response) throws 
Exception {
         MailAddress aliasSourceAddress = 
MailAddressParser.parseMailAddress(request.params(ALIAS_SOURCE_ADDRESS), 
ADDRESS_TYPE);
         ensureUserDoesNotExist(aliasSourceAddress);
         MailAddress destinationAddress = 
MailAddressParser.parseMailAddress(request.params(ALIAS_DESTINATION_ADDRESS), 
ADDRESS_TYPE);
@@ -189,14 +194,14 @@ public class AliasRoutes implements Routes {
         }
     }
 
-    private void ensureUserDoesNotExist(MailAddress mailAddress) throws 
UsersRepositoryException {
+    private void ensureUserDoesNotExist(MailAddress mailAddress) throws 
Exception {
         Username username = usersRepository.getUsername(mailAddress);
-
-        if (usersRepository.contains(username)) {
+        Optional<ValidationFailure> validationFailure = 
userEntityValidator.canCreate(username, ImmutableSet.of(ALIAS));
+        if (validationFailure.isPresent()) {
             throw ErrorResponder.builder()
-                .statusCode(HttpStatus.BAD_REQUEST_400)
-                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
-                .message("The alias source exists as an user already")
+                .statusCode(HttpStatus.CONFLICT_409)
+                .type(ErrorResponder.ErrorType.WRONG_STATE)
+                .message(validationFailure.get().errorMessage())
                 .haltError();
         }
     }
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/GroupsRoutes.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/GroupsRoutes.java
index 79135e4..ec1eca6 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/GroupsRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/GroupsRoutes.java
@@ -20,6 +20,7 @@
 package org.apache.james.webadmin.routes;
 
 import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static 
org.apache.james.webadmin.service.UserEntityValidator.EntityType.GROUP;
 import static spark.Spark.halt;
 
 import java.util.List;
@@ -44,9 +45,10 @@ import org.apache.james.rrt.lib.Mapping;
 import org.apache.james.rrt.lib.MappingSource;
 import org.apache.james.rrt.lib.Mappings;
 import org.apache.james.user.api.UsersRepository;
-import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.webadmin.Constants;
 import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.service.UserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator.ValidationFailure;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -54,6 +56,7 @@ import org.eclipse.jetty.http.HttpStatus;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedSet;
 
 import io.swagger.annotations.Api;
@@ -84,14 +87,16 @@ public class GroupsRoutes implements Routes {
     private static final String USER_ADDRESS_TYPE = "group member";
 
     private final UsersRepository usersRepository;
+    private final UserEntityValidator userEntityValidator;
     private final JsonTransformer jsonTransformer;
     private final RecipientRewriteTable recipientRewriteTable;
 
     @Inject
     @VisibleForTesting
     GroupsRoutes(RecipientRewriteTable recipientRewriteTable, UsersRepository 
usersRepository,
-                 JsonTransformer jsonTransformer) {
+                 UserEntityValidator userEntityValidator, JsonTransformer 
jsonTransformer) {
         this.usersRepository = usersRepository;
+        this.userEntityValidator = userEntityValidator;
         this.jsonTransformer = jsonTransformer;
         this.recipientRewriteTable = recipientRewriteTable;
     }
@@ -143,7 +148,7 @@ public class GroupsRoutes implements Routes {
         @ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500,
             message = "Internal server error - Something went bad on the 
server side.")
     })
-    public HaltException addToGroup(Request request, Response response) throws 
UsersRepositoryException {
+    public HaltException addToGroup(Request request, Response response) throws 
Exception {
         MailAddress groupAddress = 
MailAddressParser.parseMailAddress(request.params(GROUP_ADDRESS), 
GROUP_ADDRESS_TYPE);
         Domain domain = groupAddress.getDomain();
         ensureNotShadowingAnotherAddress(groupAddress);
@@ -179,12 +184,14 @@ public class GroupsRoutes implements Routes {
         }
     }
 
-    private void ensureNotShadowingAnotherAddress(MailAddress groupAddress) 
throws UsersRepositoryException {
-        if 
(usersRepository.contains(usersRepository.getUsername(groupAddress))) {
+    private void ensureNotShadowingAnotherAddress(MailAddress groupAddress) 
throws Exception {
+        Username username = usersRepository.getUsername(groupAddress);
+        Optional<ValidationFailure> validationFailure = 
userEntityValidator.canCreate(username, ImmutableSet.of(GROUP));
+        if (validationFailure.isPresent()) {
             throw ErrorResponder.builder()
                 .statusCode(HttpStatus.CONFLICT_409)
-                .type(ErrorType.INVALID_ARGUMENT)
-                .message("Requested group address is already used for another 
purpose")
+                .type(ErrorType.WRONG_STATE)
+                .message(validationFailure.get().errorMessage())
                 .haltError();
         }
     }
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
index 7b0b120..db02680 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/UserRoutes.java
@@ -47,7 +47,6 @@ import org.apache.james.webadmin.dto.UserResponse;
 import org.apache.james.webadmin.service.UserService;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
-import org.apache.james.webadmin.utils.JsonExtractException;
 import org.apache.james.webadmin.utils.JsonExtractor;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.Responses;
@@ -228,7 +227,7 @@ public class UserRoutes implements Routes {
         return Constants.EMPTY_BODY;
     }
 
-    private HaltException upsertUser(Request request, Response response) 
throws JsonExtractException {
+    private HaltException upsertUser(Request request, Response response) 
throws Exception {
         Username username = extractUsername(request);
         try {
             boolean isForced = request.queryParams().contains(FORCE_PARAM);
@@ -250,8 +249,8 @@ public class UserRoutes implements Routes {
             LOGGER.info(e.getMessage());
             throw ErrorResponder.builder()
                     .statusCode(HttpStatus.CONFLICT_409)
-                    .type(ErrorType.INVALID_ARGUMENT)
-                    .message("User already exists")
+                    .type(ErrorType.WRONG_STATE)
+                    .message(e.getMessage())
                     .cause(e)
                     .haltError();
         } catch (UsersRepositoryException e) {
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/AggregateUserEntityValidator.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/AggregateUserEntityValidator.java
new file mode 100644
index 0000000..8bc54d2
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/AggregateUserEntityValidator.java
@@ -0,0 +1,43 @@
+/****************************************************************
+ * 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.util.Optional;
+import java.util.Set;
+
+import org.apache.james.core.Username;
+
+import com.github.fge.lambdas.Throwing;
+
+public class AggregateUserEntityValidator implements UserEntityValidator {
+    private final Set<UserEntityValidator> validatorSet;
+
+    public AggregateUserEntityValidator(Set<UserEntityValidator> validatorSet) 
{
+        this.validatorSet = validatorSet;
+    }
+
+    @Override
+    public Optional<ValidationFailure> canCreate(Username username, 
Set<EntityType> ignoredTypes) throws Exception {
+        return validatorSet.stream()
+            .map(Throwing.<UserEntityValidator, 
Optional<ValidationFailure>>function(validator -> validator.canCreate(username, 
ignoredTypes)).sneakyThrow())
+            .flatMap(Optional::stream)
+            .findFirst();
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DefaultUserEntityValidator.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DefaultUserEntityValidator.java
new file mode 100644
index 0000000..b08a0e6
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DefaultUserEntityValidator.java
@@ -0,0 +1,49 @@
+/****************************************************************
+ * 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.util.Optional;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+
+public class DefaultUserEntityValidator implements UserEntityValidator {
+    private final UsersRepository usersRepository;
+
+    @Inject
+    public DefaultUserEntityValidator(UsersRepository usersRepository) {
+        this.usersRepository = usersRepository;
+    }
+
+    @Override
+    public Optional<ValidationFailure> canCreate(Username username, 
Set<EntityType> ignoredTypes) throws UsersRepositoryException {
+        if (ignoredTypes.contains(EntityType.USER)) {
+            return Optional.empty();
+        }
+        if (usersRepository.contains(username)) {
+            return Optional.of(new ValidationFailure("'" + username.asString() 
+ "' user already exist"));
+        }
+        return Optional.empty();
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/RecipientRewriteTableUserEntityValidator.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/RecipientRewriteTableUserEntityValidator.java
new file mode 100644
index 0000000..2274784
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/RecipientRewriteTableUserEntityValidator.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 java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableException;
+import org.apache.james.rrt.lib.Mapping;
+import org.apache.james.rrt.lib.MappingSource;
+import org.apache.james.rrt.lib.Mappings;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+public class RecipientRewriteTableUserEntityValidator implements 
UserEntityValidator {
+    private static final Map<EntityType, Mapping.Type> TYPE_CORRESPONDENCE = 
ImmutableMap.of(
+        EntityType.ALIAS, Mapping.Type.Alias,
+        EntityType.GROUP, Mapping.Type.Group);
+
+    private final RecipientRewriteTable rrt;
+
+    @Inject
+    public RecipientRewriteTableUserEntityValidator(RecipientRewriteTable rrt) 
{
+        this.rrt = rrt;
+    }
+
+    @Override
+    public Optional<ValidationFailure> canCreate(Username username, 
Set<EntityType> ignoredTypes) throws RecipientRewriteTableException {
+        Mappings mappings = 
rrt.getStoredMappings(MappingSource.fromUser(username));
+
+        return filterIgnored(mappings, ignoredTypes)
+            .findFirst()
+            .map(mapping -> new ValidationFailure("'" + username.asString() + 
"' already have associated mappings: " + mapping.asString()));
+    }
+
+    private Stream<Mapping> filterIgnored(Mappings mappings, Set<EntityType> 
ignoredTypes) {
+        final ImmutableSet<Mapping.Type> types = ignoredTypes.stream()
+            .flatMap(type -> 
Optional.ofNullable(TYPE_CORRESPONDENCE.get(type)).stream())
+            .collect(ImmutableSet.toImmutableSet());
+
+        return mappings.asStream()
+            .filter(mapping -> !types.contains(mapping.getType()));
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserEntityValidator.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserEntityValidator.java
new file mode 100644
index 0000000..5ebadf8
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserEntityValidator.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.service;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.james.core.Username;
+
+import com.google.common.collect.ImmutableSet;
+
+public interface UserEntityValidator {
+    class ValidationFailure {
+        private final String errorMessage;
+
+        public ValidationFailure(String errorMessage) {
+            this.errorMessage = errorMessage;
+        }
+
+        public String errorMessage() {
+            return errorMessage;
+        }
+    }
+
+    class EntityType {
+        public static final EntityType GROUP = new EntityType("group");
+        public static final EntityType ALIAS = new EntityType("alias");
+        public static final EntityType USER = new EntityType("user");
+
+        private final String type;
+
+        public EntityType(String type) {
+            this.type = type;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(type);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof EntityType) {
+                EntityType other = (EntityType) obj;
+                return Objects.equals(this.type, other.type);
+            }
+            return false;
+        }
+    }
+
+    UserEntityValidator NOOP = (username, ignoredTypes) -> Optional.empty();
+
+    static UserEntityValidator aggregate(UserEntityValidator... validators) {
+        return new 
AggregateUserEntityValidator(ImmutableSet.copyOf(validators));
+    }
+
+    Optional<ValidationFailure> canCreate(Username username, Set<EntityType> 
ignoredTypes) throws Exception;
+
+    default Optional<ValidationFailure> canCreate(Username username) throws 
Exception {
+        return canCreate(username, ImmutableSet.of());
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
index 2ea68b2..38cbabb 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/UserService.java
@@ -32,20 +32,18 @@ import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.api.model.User;
 import org.apache.james.util.streams.Iterators;
 import org.apache.james.webadmin.dto.UserResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.james.webadmin.service.UserEntityValidator.ValidationFailure;
 
 import com.google.common.collect.ImmutableList;
 
 public class UserService {
-
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(UserService.class);
-
     private final UsersRepository usersRepository;
+    private final UserEntityValidator userEntityValidator;
 
     @Inject
-    public UserService(UsersRepository usersRepository) {
+    public UserService(UsersRepository usersRepository, UserEntityValidator 
userEntityValidator) {
         this.usersRepository = usersRepository;
+        this.userEntityValidator = userEntityValidator;
     }
 
     public List<UserResponse> getUsers() throws UsersRepositoryException {
@@ -61,9 +59,13 @@ public class UserService {
         usersRepository.removeUser(username);
     }
 
-    public void upsertUser(Username username, char[] password) throws 
UsersRepositoryException {
+    public void upsertUser(Username username, char[] password) throws 
Exception {
         User user = usersRepository.getUserByName(username);
         if (user == null) {
+            Optional<ValidationFailure> validationFailure = 
userEntityValidator.canCreate(username);
+            if (validationFailure.isPresent()) {
+                throw new 
AlreadyExistInUsersRepositoryException(validationFailure.get().errorMessage());
+            }
             usersRepository.addUser(username, new String(password));
         } else {
             user.setPassword(new String(password));
@@ -75,13 +77,12 @@ public class UserService {
         return usersRepository.contains(username);
     }
 
-    public void insertUser(Username username, char[] password) throws 
UsersRepositoryException, AlreadyExistInUsersRepositoryException {
-        User user = usersRepository.getUserByName(username);
-        if (user == null) {
-            usersRepository.addUser(username, new String(password));
-        } else {
-            throw new AlreadyExistInUsersRepositoryException("User " + 
username + " already exists.");
+    public void insertUser(Username username, char[] password) throws 
Exception {
+        Optional<ValidationFailure> validationFailure = 
userEntityValidator.canCreate(username);
+        if (validationFailure.isPresent()) {
+            throw new 
AlreadyExistInUsersRepositoryException(validationFailure.get().errorMessage());
         }
+        usersRepository.addUser(username, new String(password));
     }
 
 }
diff --git 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
index 198d51c..7d8d09b 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/AliasRoutesTest.java
@@ -50,6 +50,9 @@ import org.apache.james.user.memory.MemoryUsersRepository;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
 import org.apache.james.webadmin.dto.MappingSourceModule;
+import org.apache.james.webadmin.service.DefaultUserEntityValidator;
+import 
org.apache.james.webadmin.service.RecipientRewriteTableUserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
@@ -59,7 +62,6 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import io.restassured.RestAssured;
-import io.restassured.filter.log.LogDetail;
 import io.restassured.http.ContentType;
 
 class AliasRoutesTest {
@@ -122,7 +124,12 @@ class AliasRoutesTest {
             usersRepository.addUser(Username.of(BOB_WITH_SLASH), 
BOB_WITH_SLASH_PASSWORD);
             usersRepository.addUser(Username.of(ALICE), ALICE_PASSWORD);
 
-            createServer(new AliasRoutes(memoryRecipientRewriteTable, 
usersRepository, domainList, new JsonTransformer(module)),
+
+            UserEntityValidator validator = UserEntityValidator.aggregate(
+                new DefaultUserEntityValidator(usersRepository),
+                new 
RecipientRewriteTableUserEntityValidator(memoryRecipientRewriteTable));
+
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, 
usersRepository, validator, domainList, new JsonTransformer(module)),
                 new AddressMappingRoutes(memoryRecipientRewriteTable));
         }
 
@@ -179,6 +186,26 @@ class AliasRoutesTest {
         }
 
         @Test
+        void putShouldDetectConflictsWithGroups() throws Exception {
+            
memoryRecipientRewriteTable.addGroupMapping(MappingSource.fromUser(Username.of(ALICE_ALIAS)),
 BOB);
+
+            Map<String, Object> errors = when()
+                    .put(ALICE + SEPARATOR + "sources" + SEPARATOR + 
ALICE_ALIAS)
+                .then()
+                    .contentType(ContentType.JSON)
+                    .statusCode(HttpStatus.CONFLICT_409)
+                    .extract()
+                    .body()
+                    .jsonPath()
+                    .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", HttpStatus.CONFLICT_409)
+                .containsEntry("type", "WrongState")
+                .containsEntry("message", "'[email protected]' already have 
associated mappings: group:[email protected]");
+        }
+
+        @Test
         void getNotRegisteredAliasesShouldReturnEmptyList() {
             when()
                 .get("[email protected]")
@@ -238,7 +265,7 @@ class AliasRoutesTest {
             Map<String, Object> errors = when()
                 .put(BOB + SEPARATOR + "sources" + SEPARATOR + ALICE)
             .then()
-                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .statusCode(HttpStatus.CONFLICT_409)
                 .contentType(ContentType.JSON)
                 .extract()
                 .body()
@@ -246,9 +273,9 @@ class AliasRoutesTest {
                 .getMap(".");
 
             assertThat(errors)
-                .containsEntry("statusCode", HttpStatus.BAD_REQUEST_400)
-                .containsEntry("type", "InvalidArgument")
-                .containsEntry("message", "The alias source exists as an user 
already");
+                .containsEntry("statusCode", HttpStatus.CONFLICT_409)
+                .containsEntry("type", "WrongState")
+                .containsEntry("message", "'[email protected]' user already exist");
         }
 
         @Test
@@ -446,7 +473,7 @@ class AliasRoutesTest {
             domainList = mock(DomainList.class);
             memoryRecipientRewriteTable.setDomainList(domainList);
             Mockito.when(domainList.containsDomain(any())).thenReturn(true);
-            createServer(new AliasRoutes(memoryRecipientRewriteTable, 
userRepository, domainList, new JsonTransformer()),
+            createServer(new AliasRoutes(memoryRecipientRewriteTable, 
userRepository, UserEntityValidator.NOOP, domainList, new JsonTransformer()),
                 new AddressMappingRoutes(memoryRecipientRewriteTable));
         }
 
diff --git 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/GroupsRoutesTest.java
 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/GroupsRoutesTest.java
index 5726d24..d461c93 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/GroupsRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/GroupsRoutesTest.java
@@ -52,6 +52,9 @@ import org.apache.james.user.memory.MemoryUsersRepository;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
 import org.apache.james.webadmin.dto.MappingSourceModule;
+import org.apache.james.webadmin.service.DefaultUserEntityValidator;
+import 
org.apache.james.webadmin.service.RecipientRewriteTableUserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
@@ -61,7 +64,6 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import io.restassured.RestAssured;
-import io.restassured.filter.log.LogDetail;
 import io.restassured.http.ContentType;
 
 class GroupsRoutesTest {
@@ -114,7 +116,10 @@ class GroupsRoutesTest {
             
memoryRecipientRewriteTable.setConfiguration(RecipientRewriteTableConfiguration.DEFAULT_ENABLED);
             usersRepository = 
MemoryUsersRepository.withVirtualHosting(domainList);
             MappingSourceModule mappingSourceModule = new 
MappingSourceModule();
-            createServer(new GroupsRoutes(memoryRecipientRewriteTable, 
usersRepository, new JsonTransformer(mappingSourceModule)),
+            UserEntityValidator validator = UserEntityValidator.aggregate(
+                new DefaultUserEntityValidator(usersRepository),
+                new 
RecipientRewriteTableUserEntityValidator(memoryRecipientRewriteTable));
+            createServer(new GroupsRoutes(memoryRecipientRewriteTable, 
usersRepository, validator, new JsonTransformer(mappingSourceModule)),
                 new AddressMappingRoutes(memoryRecipientRewriteTable));
         }
 
@@ -320,6 +325,26 @@ class GroupsRoutesTest {
         }
 
         @Test
+        void putShouldNotConflictWithAlias() throws Exception {
+            
memoryRecipientRewriteTable.addAliasMapping(MappingSource.fromUser(Username.of(GROUP1)),
 USER_A);
+
+            Map<String, Object> errors = when()
+                .put(GROUP1 + SEPARATOR + USER_A)
+            .then()
+                .contentType(ContentType.JSON)
+                .statusCode(HttpStatus.CONFLICT_409)
+                .extract()
+                .body()
+                .jsonPath()
+                .getMap(".");
+
+            assertThat(errors)
+                .containsEntry("statusCode", HttpStatus.CONFLICT_409)
+                .containsEntry("type", "WrongState")
+                .containsEntry("message", "'[email protected]' already have 
associated mappings: alias:[email protected]");
+        }
+
+        @Test
         void putUserInGroupWithEncodedSlashShouldCreateGroup() {
             when()
                 .put(GROUP_WITH_ENCODED_SLASH + SEPARATOR + USER_A);
@@ -395,8 +420,8 @@ class GroupsRoutesTest {
 
             assertThat(errors)
                 .containsEntry("statusCode", HttpStatus.CONFLICT_409)
-                .containsEntry("type", "InvalidArgument")
-                .containsEntry("message", "Requested group address is already 
used for another purpose");
+                .containsEntry("type", "WrongState")
+                .containsEntry("message", "'[email protected]' user already exist");
         }
 
         @Test
@@ -473,7 +498,7 @@ class GroupsRoutesTest {
             domainList = mock(DomainList.class);
             memoryRecipientRewriteTable.setDomainList(domainList);
             Mockito.when(domainList.containsDomain(any())).thenReturn(true);
-            createServer(new GroupsRoutes(memoryRecipientRewriteTable, 
userRepository, new JsonTransformer()),
+            createServer(new GroupsRoutes(memoryRecipientRewriteTable, 
userRepository, UserEntityValidator.NOOP, new JsonTransformer()),
                 new AddressMappingRoutes(memoryRecipientRewriteTable));
         }
 
diff --git 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/UserRoutesTest.java
 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/UserRoutesTest.java
index a5fe202..065d93b 100644
--- 
a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/UserRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/UserRoutesTest.java
@@ -55,6 +55,9 @@ import org.apache.james.user.api.model.User;
 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.DefaultUserEntityValidator;
+import 
org.apache.james.webadmin.service.RecipientRewriteTableUserEntityValidator;
+import org.apache.james.webadmin.service.UserEntityValidator;
 import org.apache.james.webadmin.service.UserService;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -118,7 +121,7 @@ class UserRoutesTest {
 
         @Override
         public void beforeEach(ExtensionContext extensionContext) {
-            webAdminServer = startServer(usersRepository);
+            webAdminServer = startServer(usersRepository, 
recipientRewriteTable);
         }
 
         @Override
@@ -145,8 +148,11 @@ class UserRoutesTest {
             throw new RuntimeException("Unknown parameter type: " + 
parameterType);
         }
 
-        private WebAdminServer startServer(UsersRepository usersRepository) {
-            WebAdminServer server = WebAdminUtils.createWebAdminServer(new 
UserRoutes(new UserService(usersRepository), canSendFrom, new 
JsonTransformer()))
+        private WebAdminServer startServer(UsersRepository usersRepository, 
RecipientRewriteTable recipientRewriteTable) {
+            UserEntityValidator validator = UserEntityValidator.aggregate(
+                new DefaultUserEntityValidator(usersRepository),
+                new 
RecipientRewriteTableUserEntityValidator(recipientRewriteTable));
+            WebAdminServer server = WebAdminUtils.createWebAdminServer(new 
UserRoutes(new UserService(usersRepository, validator), canSendFrom, new 
JsonTransformer()))
                 .start();
 
             RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(server)
@@ -409,7 +415,7 @@ class UserRoutesTest {
 
             @Test
             default void 
putShouldFailOnRepositoryExceptionOnGetUserByName(UsersRepository 
usersRepository) throws Exception {
-                
when(usersRepository.getUserByName(USERNAME_WITH_DOMAIN)).thenThrow(new 
UsersRepositoryException("message"));
+                
when(usersRepository.contains(USERNAME_WITH_DOMAIN)).thenThrow(new 
UsersRepositoryException("message"));
 
                 given()
                     .body("{\"password\":\"password\"}")
@@ -469,7 +475,7 @@ class UserRoutesTest {
 
             @Test
             default void 
putShouldFailOnUnknownExceptionOnGetUserByName(UsersRepository usersRepository) 
throws Exception {
-                
when(usersRepository.getUserByName(USERNAME_WITH_DOMAIN)).thenThrow(new 
RuntimeException());
+                
when(usersRepository.contains(USERNAME_WITH_DOMAIN)).thenThrow(new 
RuntimeException());
 
                 given()
                     .body("{\"password\":\"password\"}")
@@ -512,6 +518,8 @@ class UserRoutesTest {
     private static final Username USERNAME_WITHOUT_DOMAIN = 
Username.of("username");
     private static final Username USERNAME_WITH_DOMAIN =
         Username.fromLocalPartWithDomain(USERNAME_WITHOUT_DOMAIN.asString(), 
DOMAIN);
+    private static final Username OTHER_USERNAME_WITH_DOMAIN =
+        Username.fromLocalPartWithDomain("other", DOMAIN);
     private static final String PASSWORD = "password";
 
     @Nested
@@ -574,6 +582,38 @@ class UserRoutesTest {
         }
 
         @Test
+        void putShouldFailWhenConflictWithAlias(RecipientRewriteTable 
recipientRewriteTable) throws Exception {
+            
recipientRewriteTable.addAliasMapping(MappingSource.fromUser(USERNAME_WITH_DOMAIN),
+                OTHER_USERNAME_WITH_DOMAIN.asString());
+
+            given()
+                .body("{\"password\":\"password\"}")
+            .when()
+                .put(USERNAME_WITH_DOMAIN.asString())
+            .then()
+                .statusCode(HttpStatus.CONFLICT_409)
+                .body("statusCode", is(HttpStatus.CONFLICT_409))
+                .body("type", 
is(ErrorResponder.ErrorType.WRONG_STATE.getType()))
+                .body("message", is("'username@domain' already have associated 
mappings: alias:other@domain"));
+        }
+
+        @Test
+        void putShouldFailWhenConflictWithGroup(RecipientRewriteTable 
recipientRewriteTable) throws Exception {
+            
recipientRewriteTable.addGroupMapping(MappingSource.fromUser(USERNAME_WITH_DOMAIN),
+                OTHER_USERNAME_WITH_DOMAIN.asString());
+
+            given()
+                .body("{\"password\":\"password\"}")
+            .when()
+                .put(USERNAME_WITH_DOMAIN.asString())
+            .then()
+                .statusCode(HttpStatus.CONFLICT_409)
+                .body("statusCode", is(HttpStatus.CONFLICT_409))
+                .body("type", 
is(ErrorResponder.ErrorType.WRONG_STATE.getType()))
+                .body("message", is("'username@domain' already have associated 
mappings: group:other@domain"));
+        }
+
+        @Test
         void 
putWithDomainPartInUsernameShouldReturnOkWhenWithA255LongUsername() {
             String usernameTail = "@" + DOMAIN.name();
             given()
@@ -631,8 +671,8 @@ class UserRoutesTest {
             .then()
                 .statusCode(HttpStatus.CONFLICT_409)
                 .body("statusCode", is(HttpStatus.CONFLICT_409))
-                .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
-                .body("message", is("User already exists"));
+                .body("type", 
is(ErrorResponder.ErrorType.WRONG_STATE.getType()))
+                .body("message", is("'username@domain' user already exist"));
         }
 
         @Test
@@ -838,8 +878,8 @@ class UserRoutesTest {
             .then()
                 .statusCode(HttpStatus.CONFLICT_409)
                 .body("statusCode", is(HttpStatus.CONFLICT_409))
-                .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
-                .body("message", is("User already exists"));
+                .body("type", 
is(ErrorResponder.ErrorType.WRONG_STATE.getType()))
+                .body("message", is("'username' user already exist"));
         }
 
         @Test
diff --git a/src/site/markdown/server/manage-webadmin.md 
b/src/site/markdown/server/manage-webadmin.md
index b190270..633f4ef 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -2365,9 +2365,9 @@ Response codes:
 
  - 204: OK
  - 400: Alias structure or member is not valid
- - 400: The alias source exists as an user already
  - 400: Source and destination can't be the same!
  - 400: Domain in the destination or source is not managed by the DomainList
+ - 409: The alias source exists as an user already
  - 409: The creation of the alias would lead to a loop and thus cannot be 
performed
 
 ### Removing an alias of an user

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to