CVE-2023-35151: Exposure of Resource to Wrong Sphere (CWE-668)
value: XWiki Platform is a generic wiki platform. Starting in version 7.3-milestone-1 and prior to versions 14.4.8, 14.10.6, and 15.1, ny user can call a REST endpoint and obtain the obfuscated passwords, even when the mail obfuscation is activated. The issue has been patched in XWiki 14.4.8, 14.10.6, and 15.1. There is no known workaround.
CWE description: The product exposes a resource to the wrong control sphere, providing unintended actors with inappropriate access to the resource.
CVSS v3 base score: 7.5
Published: 2023-06-23T17:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml, xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java, xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java
Commit date: 2023-02-21 11:31:49 +0100
Commit message: XWIKI-16138: Improved REST properties cleanup

Diff: diff --git a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml
index 5733b55a12f79..783d500d72dd6 100644
--- a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/pom.xml
@@ -178,10 +178,15 @@
     <dependency>
       <groupId>org.xwiki.platform</groupId>
       <artifactId>xwiki-platform-icon-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-mail-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
 
     <!-- Test dependencies -->
     <dependency>
       <groupId>org.xwiki.commons</groupId>
       <artifactId>xwiki-commons-tool-test-component</artifactId>
diff --git a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java
index fd343d335bfeb..88af3fdb2109f 100644
--- a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java
+++ b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/main/java/org/xwiki/rest/internal/ModelFactory.java
@@ -26,17 +26,20 @@
 import java.util.Calendar;
 import java.util.Formatter;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 import java.util.Set;
 import java.util.Vector;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 import javax.inject.Singleton;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
 import javax.xml.parsers.ParserConfigurationException;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.slf4j.Logger;
@@ -44,10 +47,12 @@
 import org.xwiki.job.DefaultRequest;
 import org.xwiki.job.Request;
 import org.xwiki.logging.LogLevel;
 import org.xwiki.logging.event.LogEvent;
 import org.xwiki.logging.tail.LogTail;
+import org.xwiki.mail.EmailAddressObfuscator;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.EntityType;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.model.reference.ObjectReference;
@@ -127,10 +132,12 @@
 import com.xpn.xwiki.objects.PropertyInterface;
 import com.xpn.xwiki.objects.classes.BaseClass;
 import com.xpn.xwiki.objects.classes.ComputedFieldClass;
 import com.xpn.xwiki.objects.classes.ListClass;
 
+import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
+
 /**
  * Various common tools for resources.
  * 
  * @version $Id$
  * @since 7.3M1
@@ -165,10 +172,16 @@
     private WikiDescriptorManager wikiDescriptorManager;
 
     @Inject
     private UserReferenceSerializer<String> userReferenceSerializer;
 
+    @Inject
+    private GeneralMailConfiguration generalMailConfiguration;
+
+    @Inject
+    private EmailAddressObfuscator emailAddressObfuscator;
+
     public ModelFactory()
     {
         this.objectFactory = new ObjectFactory();
     }
 
@@ -260,11 +273,20 @@ private void fillObjectSummary(ObjectSummary objectSummary, Document doc, BaseOb
         objectSummary.setNumber(xwikiObject.getNumber());
 
         String[] propertyNames = xwikiObject.getPropertyNames();
         if (propertyNames.length > 0) {
             try {
-                objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(propertyNames[0])));
+                String firstPropertyName = propertyNames[0];
+                BaseClass baseClass = xwikiObject.getXClass(this.xcontextProvider.get());
+                PropertyInterface field = baseClass.getField(firstPropertyName);
+                // The property might not exist in the class. But if it does, it will be a PropertyClass. 
+                if (field != null) {
+                    String classType = ((com.xpn.xwiki.objects.classes.PropertyClass) field).getClassType();
+                    objectSummary.setHeadline(cleanupBeforeMakingPublic(classType, xwikiObject.get(firstPropertyName)));
+                } else {
+                    objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(firstPropertyName)));
+                }
             } catch (XWikiException e) {
                 // Should never happen
             }
         }
     }
@@ -565,11 +587,11 @@ private Locale getDefaultLocale(Document document)
             try {
                 XWikiContext xcontext = this.xcontextProvider.get();
                 return xcontext.getWiki().getDocument(document.getDocumentReference(), xcontext).getRealLocale();
             } catch (XWikiException e) {
                 this.logger.warn("Failed to get the default locale from [{}]. Root cause is [{}].",
-                    document.getDocumentReference(), ExceptionUtils.getRootCauseMessage(e));
+                    document.getDocumentReference(), getRootCauseMessage(e));
                 // Fall-back on the default locale specified on the translation page, which may not be accurate.
                 return document.getDefaultLocale();
             }
         } else {
             return document.getRealLocale();
@@ -990,11 +1012,11 @@ public Hierarchy toRestHierarchy(EntityReference targetEntityReference, Boolean
                         }
                     }
                 } catch (Exception e) {
                     this.logger.warn(
                         "Failed to get the pretty name of entity [{}]. Continue using the entity name. Root cause is [{}].",
-                        entityReference, ExceptionUtils.getRootCauseMessage(e));
+                        entityReference, getRootCauseMessage(e));
                 }
             }
             hierarchy.withItems(hierarchyItem);
         }
         return hierarchy;
@@ -1049,11 +1071,11 @@ private String serializePropertyValue(PropertyInterface property,
             } finally {
                 // Reset the context document to its original value, even if an exception is raised.
                 context.setDoc(document);
             }
         } else {
-            return serializePropertyValue(property);
+            return cleanupBeforeMakingPublic(propertyClass.getClassType(), property);
         }
     }
 
     public JobRequest toRestJobRequest(Request request) throws XWikiRestException
     {
@@ -1244,6 +1266,32 @@ private boolean hasAccess(Property restProperty)
             return authorizationManagerProvider.get().hasAccess(Right.ADMIN, xcontextProvider.get().getWikiReference());
         }
 
         return true;
     }
+
+    private String cleanupBeforeMakingPublic(String type, PropertyInterface baseProperty)
+    {
+        String cleanedUpStringValue;
+        if (Objects.equals(type, PASSWORD_TYPE)) {
+            cleanedUpStringValue = null;
+        } else {
+            cleanedUpStringValue = serializePropertyValue(baseProperty);
+            // We obfuscate the email only if the obfuscation has been activated, and if the current user does not have 
+            // the right to edit the document containing the base property.
+            // A user allowed to edit a document has to view the unescaped email to be able to edit it correctly. 
+            if (Objects.equals(type, "Email") && this.generalMailConfiguration.shouldObfuscate()
+                && !this.authorizationManagerProvider.get().hasAccess(Right.EDIT, baseProperty.getReference()))
+            {
+                try {
+                    cleanedUpStringValue =
+                        this.emailAddressObfuscator.obfuscate(InternetAddress.parse(cleanedUpStringValue)[0]);
+                } catch (AddressException e) {
+                    this.logger.warn("Failed to parse [{}] to an email address. Cause: [{}]", cleanedUpStringValue,
+                        getRootCauseMessage(e));
+                    cleanedUpStringValue = "";
+                }
+            }
+        }
+        return cleanedUpStringValue;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java
index ec7f20e91c2b1..22391bb8e2bf0 100644
--- a/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java
+++ b/xwiki-platform-core/xwiki-platform-rest/xwiki-platform-rest-server/src/test/java/org/xwiki/rest/internal/ModelFactoryTest.java
@@ -27,20 +27,28 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.Vector;
+import java.util.stream.Stream;
+
+import javax.mail.internet.InternetAddress;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.mockito.Mock;
-import org.xwiki.component.manager.ComponentManager;
+import org.xwiki.mail.EmailAddressObfuscator;
+import org.xwiki.mail.GeneralMailConfiguration;
 import org.xwiki.model.reference.AttachmentReference;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.model.reference.ObjectReference;
 import org.xwiki.model.reference.SpaceReference;
 import org.xwiki.model.reference.SpaceReferenceResolver;
 import org.xwiki.model.reference.WikiReference;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.rest.model.jaxb.Attachment;
@@ -52,32 +60,36 @@
 import org.xwiki.rest.model.jaxb.Translations;
 import org.xwiki.security.authorization.ContextualAuthorizationManager;
 import org.xwiki.security.authorization.Right;
 import org.xwiki.test.LogLevel;
 import org.xwiki.test.junit5.LogCaptureExtension;
-import org.xwiki.test.junit5.mockito.InjectComponentManager;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.wiki.descriptor.WikiDescriptor;
 import org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
 import com.xpn.xwiki.XWiki;
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.XWikiException;
 import com.xpn.xwiki.api.Document;
 import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.objects.BaseCollection;
 import com.xpn.xwiki.objects.BaseObject;
 import com.xpn.xwiki.objects.StringProperty;
 import com.xpn.xwiki.objects.classes.BaseClass;
+import com.xpn.xwiki.objects.classes.EmailClass;
 import com.xpn.xwiki.objects.classes.PasswordClass;
 import com.xpn.xwiki.objects.classes.StringClass;
 import com.xpn.xwiki.test.MockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
 import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
 
+import ch.qos.logback.classic.Level;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 @OldcoreTest
 class ModelFactoryTest
@@ -91,13 +103,10 @@
     private static final String TEST_PASSWORD_VALUE = "secret";
 
     @RegisterExtension
     private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
 
-    @InjectComponentManager
-    private ComponentManager componentManager;
-
     @MockComponent
     private ContextualAuthorizationManager authorizationManager;
 
     @MockComponent
     private SpaceReferenceResolver<String> resolver;
@@ -106,10 +115,16 @@
     private WikiDescriptorManager wikiDescriptorManager;
 
     @MockComponent
     private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
 
+    @MockComponent
+    private GeneralMailConfiguration generalMailConfiguration;
+
+    @MockComponent
+    private EmailAddressObfuscator emailAddressObfuscator;
+
     @InjectMockComponents
     private ModelFactory modelFactory;
 
     @InjectMockitoOldcore
     private MockitoOldcore oldCore;
@@ -204,14 +219,105 @@ void toRestObjectCheckWhichObjectValuesAreAvailableForAdmins() throws Exception
 
         Object result = modelFactory.toRestObject(baseURI, testDocument, xwikiObject, false, false);
 
         Map<String, String> expectedValues = new HashMap<>();
         expectedValues.put(TEST_STRING_FIELD, TEST_STRING_VALUE);
-        expectedValues.put(TEST_PASSWORD_FIELD, TEST_PASSWORD_VALUE);
+        expectedValues.put(TEST_PASSWORD_FIELD, null);
         assertExpectedPropertyValues(result.getProperties(), expectedValues);
     }
 
+    public static Stream<Arguments> toRestObjectWithObfuscatedMailSource()
+    {
+        return Stream.of(
+            // no mail obfuscation
+            Arguments.of(false, true, "user@domain.tld", "user@domain.tld", null),
+            // mail obfuscation activated but the current user has edit rights
+            Arguments.of(true, true, "user@domain.tld", "user@domain.tld", null),
+            // mail obfuscation activated and the current user does not have edit rights 
+            Arguments.of(true, false, "user@domain.tld", "u...@domain.tld", null),
+            // mail obfuscation activated, the current user does not have edit rights and the mail is badly formatted
+            Arguments.of(true, false, "wrong@", "",
+                "Failed to parse [wrong@] to an email address. Cause: [AddressException: Missing domain]")
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("toRestObjectWithObfuscatedMailSource")
+    void toRestObjectWithObfuscatedMail(boolean shouldObfuscate, boolean hasEditRight, String inputMail,
+        String expectedEmail, String expectedWarning) throws Exception
+    {
+        ObjectReference objectReference = mock(ObjectReference.class);
+        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(shouldObfuscate);
+        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(hasEditRight);
+        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn(expectedEmail);
+
+        BaseObject xwikiObject = mock(BaseObject.class);
+        BaseClass xwikiClass = mock(BaseClass.class);
+
+        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
+        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
+        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
+        when(xwikiObject.getNumber()).thenReturn(0);
+
+        EmailClass emailField = new EmailClass();
+        emailField.setName("emailValue");
+        StringProperty emailElement = new StringProperty();
+        emailElement.setName("emailValue");
+        emailElement.setClassType("Password");
+        emailElement.setValue(inputMail);
+        BaseCollection baseCollection = mock(BaseCollection.class);
+        when(baseCollection.getReference()).thenReturn(objectReference);
+        emailElement.setObject(baseCollection);
+        when(xwikiObject.get("emailValue")).thenReturn(emailElement);
+
+        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });
+
+        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);
+
+        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", expectedEmail));
+
+        if (expectedWarning != null) {
+            assertEquals(expectedWarning, this.logCapture.getMessage(0));
+            assertEquals(Level.WARN, this.logCapture.getLogEvent(0).getLevel());
+        }
+    }
+
+    @Test
+    void toRestObjectWithObfuscatedMailInHeader() throws Exception
+    {
+        ObjectReference objectReference = mock(ObjectReference.class);
+        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(true);
+        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(false);
+        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn("u...@domain.tld");
+
+        BaseObject xwikiObject = mock(BaseObject.class);
+        BaseClass xwikiClass = mock(BaseClass.class);
+
+        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
+        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
+        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
+        when(xwikiObject.getNumber()).thenReturn(0);
+
+        EmailClass emailField = new EmailClass();
+        emailField.setName("emailValue");
+        StringProperty emailElement = new StringProperty();
+        emailElement.setName("emailValue");
+        emailElement.setClassType("Password");
+        emailElement.setValue("user@domain.tld");
+        BaseCollection baseCollection = mock(BaseCollection.class);
+        when(baseCollection.getReference()).thenReturn(objectReference);
+        emailElement.setObject(baseCollection);
+        when(xwikiObject.get("emailValue")).thenReturn(emailElement);
+
+        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });
+        when(xwikiObject.getPropertyNames()).thenReturn(new String[] { "emailValue" });
+
+        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);
+
+        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", "u...@domain.tld"));
+    }
+
     private void assertExpectedPropertyValues(List<Property> properties, Map<String, String> expectedValues)
     {
         Set<String> propertiesFound = new HashSet<>();
         for (Property prop : properties) {
             String expectedValue = expectedValues.get(prop.getName());

Vulnerable code:                 objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(propertyNames[0])));
                    document.getDocumentReference(), ExceptionUtils.getRootCauseMessage(e));
                        entityReference, ExceptionUtils.getRootCauseMessage(e));
            return serializePropertyValue(property);
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.test.junit5.mockito.InjectComponentManager;
    @InjectComponentManager
    private ComponentManager componentManager;

        expectedValues.put(TEST_PASSWORD_FIELD, TEST_PASSWORD_VALUE);
Fixed code:     <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-mail-api</artifactId>
      <version>${project.version}</version>
    </dependency>
import java.util.Objects;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import org.xwiki.mail.EmailAddressObfuscator;
import org.xwiki.mail.GeneralMailConfiguration;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;

    @Inject
    private GeneralMailConfiguration generalMailConfiguration;

    @Inject
    private EmailAddressObfuscator emailAddressObfuscator;

                String firstPropertyName = propertyNames[0];
                BaseClass baseClass = xwikiObject.getXClass(this.xcontextProvider.get());
                PropertyInterface field = baseClass.getField(firstPropertyName);
                // The property might not exist in the class. But if it does, it will be a PropertyClass. 
                if (field != null) {
                    String classType = ((com.xpn.xwiki.objects.classes.PropertyClass) field).getClassType();
                    objectSummary.setHeadline(cleanupBeforeMakingPublic(classType, xwikiObject.get(firstPropertyName)));
                } else {
                    objectSummary.setHeadline(serializePropertyValue(xwikiObject.get(firstPropertyName)));
                }
                    document.getDocumentReference(), getRootCauseMessage(e));
                        entityReference, getRootCauseMessage(e));
            return cleanupBeforeMakingPublic(propertyClass.getClassType(), property);

    private String cleanupBeforeMakingPublic(String type, PropertyInterface baseProperty)
    {
        String cleanedUpStringValue;
        if (Objects.equals(type, PASSWORD_TYPE)) {
            cleanedUpStringValue = null;
        } else {
            cleanedUpStringValue = serializePropertyValue(baseProperty);
            // We obfuscate the email only if the obfuscation has been activated, and if the current user does not have 
            // the right to edit the document containing the base property.
            // A user allowed to edit a document has to view the unescaped email to be able to edit it correctly. 
            if (Objects.equals(type, "Email") && this.generalMailConfiguration.shouldObfuscate()
                && !this.authorizationManagerProvider.get().hasAccess(Right.EDIT, baseProperty.getReference()))
            {
                try {
                    cleanedUpStringValue =
                        this.emailAddressObfuscator.obfuscate(InternetAddress.parse(cleanedUpStringValue)[0]);
                } catch (AddressException e) {
                    this.logger.warn("Failed to parse [{}] to an email address. Cause: [{}]", cleanedUpStringValue,
                        getRootCauseMessage(e));
                    cleanedUpStringValue = "";
                }
            }
        }
        return cleanedUpStringValue;
    }
import java.util.stream.Stream;

import javax.mail.internet.InternetAddress;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.xwiki.mail.EmailAddressObfuscator;
import org.xwiki.mail.GeneralMailConfiguration;
import org.xwiki.model.reference.ObjectReference;
import com.xpn.xwiki.objects.BaseCollection;
import com.xpn.xwiki.objects.classes.EmailClass;
import ch.qos.logback.classic.Level;

import static org.mockito.ArgumentMatchers.any;
    @MockComponent
    private GeneralMailConfiguration generalMailConfiguration;

    @MockComponent
    private EmailAddressObfuscator emailAddressObfuscator;

        expectedValues.put(TEST_PASSWORD_FIELD, null);
    public static Stream<Arguments> toRestObjectWithObfuscatedMailSource()
    {
        return Stream.of(
            // no mail obfuscation
            Arguments.of(false, true, "user@domain.tld", "user@domain.tld", null),
            // mail obfuscation activated but the current user has edit rights
            Arguments.of(true, true, "user@domain.tld", "user@domain.tld", null),
            // mail obfuscation activated and the current user does not have edit rights 
            Arguments.of(true, false, "user@domain.tld", "u...@domain.tld", null),
            // mail obfuscation activated, the current user does not have edit rights and the mail is badly formatted
            Arguments.of(true, false, "wrong@", "",
                "Failed to parse [wrong@] to an email address. Cause: [AddressException: Missing domain]")
        );
    }

    @ParameterizedTest
    @MethodSource("toRestObjectWithObfuscatedMailSource")
    void toRestObjectWithObfuscatedMail(boolean shouldObfuscate, boolean hasEditRight, String inputMail,
        String expectedEmail, String expectedWarning) throws Exception
    {
        ObjectReference objectReference = mock(ObjectReference.class);
        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(shouldObfuscate);
        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(hasEditRight);
        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn(expectedEmail);

        BaseObject xwikiObject = mock(BaseObject.class);
        BaseClass xwikiClass = mock(BaseClass.class);

        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
        when(xwikiObject.getNumber()).thenReturn(0);

        EmailClass emailField = new EmailClass();
        emailField.setName("emailValue");
        StringProperty emailElement = new StringProperty();
        emailElement.setName("emailValue");
        emailElement.setClassType("Password");
        emailElement.setValue(inputMail);
        BaseCollection baseCollection = mock(BaseCollection.class);
        when(baseCollection.getReference()).thenReturn(objectReference);
        emailElement.setObject(baseCollection);
        when(xwikiObject.get("emailValue")).thenReturn(emailElement);

        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });

        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);

        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", expectedEmail));

        if (expectedWarning != null) {
            assertEquals(expectedWarning, this.logCapture.getMessage(0));
            assertEquals(Level.WARN, this.logCapture.getLogEvent(0).getLevel());
        }
    }

    @Test
    void toRestObjectWithObfuscatedMailInHeader() throws Exception
    {
        ObjectReference objectReference = mock(ObjectReference.class);
        when(this.generalMailConfiguration.shouldObfuscate()).thenReturn(true);
        when(this.authorizationManager.hasAccess(Right.EDIT, objectReference)).thenReturn(false);
        when(this.emailAddressObfuscator.obfuscate(any(InternetAddress.class))).thenReturn("u...@domain.tld");

        BaseObject xwikiObject = mock(BaseObject.class);
        BaseClass xwikiClass = mock(BaseClass.class);

        when(xwikiObject.getPropertyNames()).thenReturn(new String[] {});
        when(xwikiObject.getXClass(this.xcontext)).thenReturn(xwikiClass);
        when(xwikiObject.getClassName()).thenReturn("Some.XClass");
        when(xwikiObject.getNumber()).thenReturn(0);

        EmailClass emailField = new EmailClass();
        emailField.setName("emailValue");
        StringProperty emailElement = new StringProperty();
        emailElement.setName("emailValue");
        emailElement.setClassType("Password");
        emailElement.setValue("user@domain.tld");
        BaseCollection baseCollection = mock(BaseCollection.class);
        when(baseCollection.getReference()).thenReturn(objectReference);
        emailElement.setObject(baseCollection);
        when(xwikiObject.get("emailValue")).thenReturn(emailElement);

        when(xwikiClass.getProperties()).thenReturn(new java.lang.Object[] { emailField });
        when(xwikiObject.getPropertyNames()).thenReturn(new String[] { "emailValue" });

        Object result = this.modelFactory.toRestObject(this.baseURI, this.testDocument, xwikiObject, false, false);

        assertExpectedPropertyValues(result.getProperties(), Map.of("emailValue", "u...@domain.tld"));
    }