You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by an...@apache.org on 2022/01/14 16:02:35 UTC

[syncope] branch 2_1_X updated: [SYNCOPE-1656] remediation now is generated also on pull update failure (#302)

This is an automated email from the ASF dual-hosted git repository.

andreapatricelli pushed a commit to branch 2_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/2_1_X by this push:
     new 111cd77  [SYNCOPE-1656] remediation now is generated also on pull update failure (#302)
111cd77 is described below

commit 111cd779dc400ff567e3362e0e982b64354bf3f5
Author: Andrea Patricelli <an...@apache.org>
AuthorDate: Fri Jan 14 17:02:28 2022 +0100

    [SYNCOPE-1656] remediation now is generated also on pull update failure (#302)
    
    * [SYNCOPE-1656] remediation now is generated also on pull update failure, made some refactoring
---
 .../java/pushpull/AbstractPullResultHandler.java   | 127 +++++++++++++--------
 .../pushpull/DefaultUserPullResultHandler.java     |  56 +++------
 .../org/apache/syncope/fit/AbstractITCase.java     |  32 ++++++
 .../apache/syncope/fit/core/PullTaskITCase.java    | 115 ++++++++++++++++++-
 4 files changed, 243 insertions(+), 87 deletions(-)

diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
index 2d84f06..3416c3e 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
@@ -18,57 +18,60 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.patch.AnyPatch;
 import org.apache.syncope.common.lib.patch.StringPatchItem;
 import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.AuditElements;
 import org.apache.syncope.common.lib.types.AuditElements.Result;
 import org.apache.syncope.common.lib.types.MatchType;
 import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.PatchOperation;
-import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.common.lib.types.PullMode;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.persistence.api.dao.RemediationDAO;
+import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
-import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.Remediation;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.provisioning.api.AuditManager;
+import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
+import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheKey;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheValue;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
-import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
-import org.apache.syncope.core.persistence.api.dao.PullMatch;
-import org.apache.syncope.core.persistence.api.dao.TaskDAO;
-import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheKey;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullResultHandler;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
 
 @Transactional(rollbackFor = Throwable.class)
 public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHandler<PullTask, PullActions>
@@ -90,6 +93,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     protected UserDAO userDAO;
 
     @Autowired
+    protected AnyTypeDAO anyTypeDAO;
+
+    @Autowired
     protected TaskDAO taskDAO;
 
     @Autowired
@@ -250,18 +256,12 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                 if (profile.getTask().isRemediation()) {
                     // set to SUCCESS to let the incremental flow go on in case of errors
                     resultStatus = Result.SUCCESS;
-                    Remediation entity = entityFactory.newEntity(Remediation.class);
-                    entity.setAnyType(provision.getAnyType());
-                    entity.setOperation(ResourceOperation.CREATE);
-                    entity.setPayload(anyTO);
-                    entity.setError(result.getMessage());
-                    entity.setInstant(new Date());
-                    entity.setRemoteName(delta.getObject().getName().getNameValue());
-                    if (taskDAO.find(profile.getTask().getKey()) != null) {
-                        entity.setPullTask(profile.getTask());
-                    }
-
-                    remediationDAO.save(entity);
+                    createRemediation(
+                            provision.getAnyType(),
+                            anyTO,
+                            taskDAO.find(profile.getTask().getKey()) != null ? profile.getTask() : null,
+                            result,
+                            delta);
                 } else {
                     resultStatus = Result.FAILURE;
                 }
@@ -378,17 +378,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         if (profile.getTask().isRemediation()) {
                             // set to SUCCESS to let the incremental flow go on in case of errors
                             resultStatus = Result.SUCCESS;
-
-                            Remediation entity = entityFactory.newEntity(Remediation.class);
-                            entity.setAnyType(provision.getAnyType());
-                            entity.setOperation(ResourceOperation.UPDATE);
-                            entity.setPayload(anyPatch);
-                            entity.setError(result.getMessage());
-                            entity.setInstant(new Date());
-                            entity.setRemoteName(delta.getObject().getName().getNameValue());
-                            entity.setPullTask(profile.getTask());
-
-                            remediationDAO.save(entity);
+                            createRemediation(provision.getAnyType(), anyPatch, profile.getTask(), result, delta);
                         } else {
                             resultStatus = Result.FAILURE;
                         }
@@ -682,16 +672,8 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         if (profile.getTask().isRemediation()) {
                             // set to SUCCESS to let the incremental flow go on in case of errors
                             resultStatus = Result.SUCCESS;
-                            Remediation entity = entityFactory.newEntity(Remediation.class);
-                            entity.setAnyType(provision.getAnyType());
-                            entity.setOperation(ResourceOperation.DELETE);
-                            entity.setPayload(match.getAny().getKey());
-                            entity.setError(result.getMessage());
-                            entity.setInstant(new Date());
-                            entity.setRemoteName(delta.getObject().getName().getNameValue());
-                            entity.setPullTask(profile.getTask());
-
-                            remediationDAO.save(entity);
+                            createRemediation(
+                                    provision.getAnyType(), match.getAny().getKey(), profile.getTask(), result, delta);
                         }
                     }
 
@@ -948,4 +930,59 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                 delta,
                 furtherInput);
     }
+
+    protected Remediation createRemediation(
+            final AnyType anyType,
+            final String anyKey,
+            final PullTask pullTask,
+            final ProvisioningReport result,
+            final SyncDelta delta) {
+        return createRemediation(anyType, anyKey, null, null, pullTask, result, delta);
+    }
+
+    protected Remediation createRemediation(
+            final AnyType anyType,
+            final AnyTO anyTO,
+            final PullTask pullTask,
+            final ProvisioningReport result,
+            final SyncDelta delta) {
+        return createRemediation(anyType, null, anyTO, null, pullTask, result, delta);
+    }
+
+    protected Remediation createRemediation(
+            final AnyType anyType,
+            final AnyPatch anyPatch,
+            final PullTask pullTask,
+            final ProvisioningReport result,
+            final SyncDelta delta) {
+        return createRemediation(anyType, null, null, anyPatch, pullTask, result, delta);
+    }
+
+    protected Remediation createRemediation(
+            final AnyType anyType,
+            final String anyKey,
+            final AnyTO anyTO,
+            final AnyPatch anyPatch,
+            final PullTask pullTask,
+            final ProvisioningReport result,
+            final SyncDelta delta) {
+        Remediation entity = entityFactory.newEntity(Remediation.class);
+
+        entity.setAnyType(anyType);
+        entity.setOperation(anyPatch == null ? ResourceOperation.CREATE : ResourceOperation.UPDATE);
+        if (StringUtils.isNotBlank(anyKey)) {
+            entity.setPayload(anyKey);
+        } else if (anyTO != null) {
+            entity.setPayload(anyTO);
+        } else if (anyPatch != null) {
+            entity.setPayload(anyPatch);
+        }
+        entity.setError(result.getMessage());
+        entity.setInstant(new Date());
+        entity.setRemoteName(delta.getObject().getName().getNameValue());
+        entity.setPullTask(pullTask);
+
+        return remediationDAO.save(entity);
+    }
+
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
index d54e9d3..6194e28 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
@@ -18,13 +18,6 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.commons.lang3.tuple.Pair;
@@ -35,6 +28,7 @@ import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.AttrTO;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.PropagationStatus;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.AuditElements;
@@ -47,7 +41,6 @@ import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
-import org.apache.syncope.core.persistence.api.entity.Remediation;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.persistence.api.entity.user.User;
@@ -56,14 +49,19 @@ import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
-import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
-import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.apache.syncope.core.provisioning.api.pushpull.UserPullResultHandler;
 import org.identityconnectors.framework.common.objects.AttributeUtil;
+import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 
 public class DefaultUserPullResultHandler extends AbstractPullResultHandler implements UserPullResultHandler {
 
@@ -127,6 +125,11 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 Collections.singleton(profile.getTask().getResource().getKey()),
                 true);
 
+        if (ProvisioningReport.Status.FAILURE == result.getStatus() && profile.getTask().isRemediation()) {
+            createRemediation(anyTypeDAO.find(result.getAnyType()), anyPatch, profile.getTask(), result,
+                    delta);
+        }
+
         return updated.getLeft();
     }
 
@@ -395,16 +398,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 resultStatus = Result.FAILURE;
 
                 if (profile.getTask().isRemediation()) {
-                    Remediation entity = entityFactory.newEntity(Remediation.class);
-                    entity.setAnyType(provision.getAnyType());
-                    entity.setOperation(ResourceOperation.UPDATE);
-                    entity.setPayload(patch);
-                    entity.setError(report.getMessage());
-                    entity.setInstant(new Date());
-                    entity.setRemoteName(delta.getObject().getName().getNameValue());
-                    entity.setPullTask(profile.getTask());
-
-                    remediationDAO.save(entity);
+                    createRemediation(provision.getAnyType(), patch, profile.getTask(), report, delta);
                 }
             }
 
@@ -520,16 +514,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                 resultStatus = Result.FAILURE;
 
                 if (profile.getTask().isRemediation()) {
-                    Remediation entity = entityFactory.newEntity(Remediation.class);
-                    entity.setAnyType(provision.getAnyType());
-                    entity.setOperation(ResourceOperation.UPDATE);
-                    entity.setPayload(patch);
-                    entity.setError(report.getMessage());
-                    entity.setInstant(new Date());
-                    entity.setRemoteName(delta.getObject().getName().getNameValue());
-                    entity.setPullTask(profile.getTask());
-
-                    remediationDAO.save(entity);
+                    createRemediation(provision.getAnyType(), patch, profile.getTask(), report, delta);
                 }
             }
 
@@ -601,16 +586,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                     output = e;
 
                     if (profile.getTask().isRemediation()) {
-                        Remediation entity = entityFactory.newEntity(Remediation.class);
-                        entity.setAnyType(provision.getAnyType());
-                        entity.setOperation(ResourceOperation.UPDATE);
-                        entity.setPayload(patch);
-                        entity.setError(report.getMessage());
-                        entity.setInstant(new Date());
-                        entity.setRemoteName(delta.getObject().getName().getNameValue());
-                        entity.setPullTask(profile.getTask());
-
-                        remediationDAO.save(entity);
+                        createRemediation(provision.getAnyType(), patch, profile.getTask(), report, delta);
                     }
                 }
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index f54f968..a79d315 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -34,10 +34,13 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.UUID;
 import javax.naming.Context;
 import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
 import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
 import javax.naming.directory.DirContext;
 import javax.naming.directory.InitialDirContext;
 import javax.naming.directory.ModificationItem;
@@ -606,6 +609,34 @@ public abstract class AbstractITCase {
         }
     }
 
+    protected void createLdapRemoteObject(
+            final String bindDn,
+            final String bindPwd,
+            final Pair<String, Set<Attribute>> entryAttrs) throws NamingException {
+
+        InitialDirContext ctx = null;
+        try {
+            ctx = getLdapResourceDirContext(bindDn, bindPwd);
+
+            BasicAttributes entry = new BasicAttributes();
+            entryAttrs.getRight().forEach(item -> entry.put(item));
+
+            ctx.createSubcontext(entryAttrs.getLeft(), entry);
+
+        } catch (NamingException e) {
+            LOG.error("While creating {} with {}", entryAttrs.getLeft(), entryAttrs.getRight(), e);
+            throw e;
+        } finally {
+            if (ctx != null) {
+                try {
+                    ctx.close();
+                } catch (NamingException e) {
+                    // ignore
+                }
+            }
+        }
+    }
+
     protected void updateLdapRemoteObject(
             final String bindDn,
             final String bindPwd,
@@ -702,4 +733,5 @@ public abstract class AbstractITCase {
         } while (results.isEmpty() && i < maxWaitSeconds);
         return results;
     }
+
 }
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
index f66f8ba..bb2e545 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
@@ -42,10 +42,15 @@ import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
 import java.util.UUID;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.BasicAttribute;
 import javax.sql.DataSource;
 import javax.ws.rs.core.Response;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.SerializationUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.commons.lang3.tuple.Triple;
 import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
@@ -867,8 +872,7 @@ public class PullTaskITCase extends AbstractTaskITCase {
                     "SyncopeClientCompositeException: {[RequiredValuesMissing [userId]]}"));
         } finally {
             resourceService.delete(ldap.getKey());
-            remediationService.list(new RemediationQuery.Builder().page(1).size(10).build()).getResult().forEach(
-                    r -> remediationService.delete(r.getKey()));
+            cleanUpRemediations();
         }
     }
 
@@ -1394,4 +1398,111 @@ public class PullTaskITCase extends AbstractTaskITCase {
             }
         }
     }
+
+    @Test
+    public void issueSYNCOPE1656() throws NamingException {
+        // preliminary create a new LDAP object
+        createLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, prepareLdapAttributes(
+                "pullFromLDAP_issue1656",
+                "pullFromLDAP_issue1656@syncope.apache.org",
+                "Active",
+                "pullFromLDAP_issue1656",
+                "Surname",
+                "5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8",
+                "odd",
+                "password"));
+        try {
+            // 1. Pull from resource-ldap
+            PullTaskTO pullTask = new PullTaskTO();
+            pullTask.setDestinationRealm(SyncopeConstants.ROOT_REALM);
+            pullTask.setName("SYNCOPE1656");
+            pullTask.setActive(true);
+            pullTask.setPerformCreate(true);
+            pullTask.setPerformUpdate(true);
+            pullTask.setRemediation(true);
+            pullTask.setPullMode(PullMode.FULL_RECONCILIATION);
+            pullTask.setResource(RESOURCE_NAME_LDAP);
+
+            Response taskResponse = taskService.create(TaskType.PULL, pullTask);
+            pullTask = getObject(taskResponse.getLocation(), TaskService.class, PullTaskTO.class);
+            assertNotNull(pullTask);
+
+            ExecTO execution = execProvisioningTask(
+                    taskService, TaskType.PULL, pullTask.getKey(), MAX_WAIT_SECONDS, false);
+            assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(execution.getStatus()));
+
+            UserTO pullFromLDAP_issue1656 = userService.read("pullFromLDAP_issue1656");
+            assertEquals("pullFromLDAP_issue1656@syncope.apache.org",
+                    pullFromLDAP_issue1656.getPlainAttr("email").get().getValues().get(0));
+            // 2. Edit mail attribute directly on the resource in order to have a not valid email
+            ConnObjectTO connObject =
+                    resourceService.readConnObject(RESOURCE_NAME_LDAP, AnyTypeKind.USER.name(),
+                            pullFromLDAP_issue1656.getKey());
+            assertNotNull(connObject);
+            assertEquals("pullFromLDAP_issue1656@syncope.apache.org",
+                    connObject.getAttr("mail").get().getValues().get(0));
+            AttrTO userDn = connObject.getAttr(Name.NAME).get();
+            assertNotNull(userDn);
+            assertEquals(1, userDn.getValues().size());
+            updateLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD,
+                    userDn.getValues().get(0), Collections.singletonMap("mail", "pullFromLDAP_issue1656@"));
+            // 3. Pull again from resource-ldap
+            execution = execProvisioningTask(taskService, TaskType.PULL, pullTask.getKey(), MAX_WAIT_SECONDS, false);
+            assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(execution.getStatus()));
+            assertTrue(execution.getMessage().contains("UPDATE FAILURE"));
+            pullFromLDAP_issue1656 = userService.read("pullFromLDAP_issue1656");
+            assertEquals("pullFromLDAP_issue1656@syncope.apache.org",
+                    pullFromLDAP_issue1656.getPlainAttr("email").get().getValues().get(0));
+            final String pullFromLDAP_issue1656_key = pullFromLDAP_issue1656.getKey();
+            // a remediation for pullFromLDAP_issue1656 email should have been created
+            PagedResult<RemediationTO> remediations =
+                    remediationService.list(new RemediationQuery.Builder().page(1).size(10).build());
+            assertTrue(remediations.getResult().stream().anyMatch(
+                    r -> pullFromLDAP_issue1656_key.equals(r.getAnyPatchPayload().getKey())));
+            assertTrue(remediations.getResult().stream().anyMatch(r -> StringUtils.contains(r.getError(),
+                    "\"pullFromLDAP_issue1656@\" is not a valid email address")));
+        } finally {
+            // remove test entity
+            removeLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD,
+                    "uid=pullFromLDAP_issue1656,ou=People,o=isp");
+            cleanUpRemediations();
+        }
+    }
+
+    private void cleanUpRemediations() {
+        remediationService.list(new RemediationQuery.Builder().page(1).size(100).build()).getResult().forEach(
+                r -> remediationService.delete(r.getKey()));
+    }
+
+    private Pair<String, Set<Attribute>> prepareLdapAttributes(
+            final String uid,
+            final String email,
+            final String description,
+            final String givenName,
+            final String sn,
+            final String registeredAddress,
+            final String title,
+            final String password) {
+        String entryDn = "uid=" + uid + ",ou=People,o=isp";
+        Set<Attribute> attributes = new HashSet<>();
+
+        attributes.add(new BasicAttribute("description", description));
+        attributes.add(new BasicAttribute("givenName", givenName));
+        attributes.add(new BasicAttribute("mail", email));
+        attributes.add(new BasicAttribute("sn", sn));
+        attributes.add(new BasicAttribute("cn", uid));
+        attributes.add(new BasicAttribute("uid", uid));
+        attributes.add(new BasicAttribute("registeredaddress", registeredAddress));
+        attributes.add(new BasicAttribute("title", title));
+        attributes.add(new BasicAttribute("userpassword", password));
+
+        Attribute oc = new BasicAttribute("objectClass");
+        oc.add("top");
+        oc.add("person");
+        oc.add("inetOrgPerson");
+        oc.add("organizationalPerson");
+        attributes.add(oc);
+
+        return Pair.of(entryDn, attributes);
+    }
 }