syncope-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ilgro...@apache.org
Subject [1/2] syncope git commit: [SYNCOPE-1287] Core implementation
Date Wed, 28 Mar 2018 14:53:26 GMT
Repository: syncope
Updated Branches:
  refs/heads/master 59a3422e4 -> 51b314fe5


http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/logic/src/main/java/org/apache/syncope/core/logic/SchemaLogic.java
----------------------------------------------------------------------
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/SchemaLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/SchemaLogic.java
index d9cfafa..892850e 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/SchemaLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/SchemaLogic.java
@@ -48,6 +48,7 @@ import org.apache.syncope.core.provisioning.api.data.SchemaDataBinder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @Component
 public class SchemaLogic extends AbstractTransactionalLogic<SchemaTO> {
@@ -145,11 +146,13 @@ public class SchemaLogic extends AbstractTransactionalLogic<SchemaTO> {
     }
 
     @PreAuthorize("isAuthenticated()")
+    @Transactional(readOnly = true)
     public <T extends SchemaTO> List<T> list(final SchemaType schemaType, final List<String> anyTypeClasses) {
         return doSearch(schemaType, anyTypeClasses, null);
     }
 
     @PreAuthorize("isAuthenticated()")
+    @Transactional(readOnly = true)
     public <T extends SchemaTO> List<T> search(
             final SchemaType schemaType, final List<String> anyTypeClasses, final String keyword) {
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/logic/src/main/java/org/apache/syncope/core/logic/SecurityQuestionLogic.java
----------------------------------------------------------------------
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/SecurityQuestionLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/SecurityQuestionLogic.java
index f77f2fd..46285f2 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/SecurityQuestionLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/SecurityQuestionLogic.java
@@ -50,8 +50,7 @@ public class SecurityQuestionLogic extends AbstractTransactionalLogic<SecurityQu
     @PreAuthorize("isAuthenticated()")
     @Transactional(readOnly = true)
     public List<SecurityQuestionTO> list() {
-        return securityQuestionDAO.findAll().stream().
-                map(securityQuestion -> binder.getSecurityQuestionTO(securityQuestion)).collect(Collectors.toList());
+        return securityQuestionDAO.findAll().stream().map(binder::getSecurityQuestionTO).collect(Collectors.toList());
     }
 
     @PreAuthorize("hasRole('" + StandardEntitlement.SECURITY_QUESTION_READ + "')")

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
----------------------------------------------------------------------
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
index 0fb587a..8aa7151 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/SyncopeLogic.java
@@ -23,6 +23,7 @@ import java.lang.management.OperatingSystemMXBean;
 import java.lang.management.RuntimeMXBean;
 import java.lang.reflect.Method;
 import java.net.InetAddress;
+import java.net.URI;
 import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.Collections;
@@ -68,6 +69,7 @@ import org.apache.syncope.core.persistence.api.dao.search.AttributeCond;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.group.TypeExtension;
 import org.apache.syncope.core.persistence.api.entity.policy.AccountPolicy;
@@ -210,8 +212,8 @@ public class SyncopeLogic extends AbstractLogic<AbstractBaseBean> {
                 PLATFORM_INFO.setBuildNumber(buildNumber);
 
                 if (bundleManager.getLocations() != null) {
-                    bundleManager.getLocations().
-                            forEach(location -> PLATFORM_INFO.getConnIdLocations().add(location.toASCIIString()));
+                    PLATFORM_INFO.getConnIdLocations().addAll(bundleManager.getLocations().stream().
+                            map(URI::toASCIIString).collect(Collectors.toList()));
                 }
 
                 PLATFORM_INFO.setPropagationTaskExecutor(AopUtils.getTargetClass(propagationTaskExecutor).getName());
@@ -246,19 +248,19 @@ public class SyncopeLogic extends AbstractLogic<AbstractBaseBean> {
             AuthContextUtils.execWithAuthContext(AuthContextUtils.getDomain(), () -> {
                 PLATFORM_INFO.getAnyTypes().clear();
                 PLATFORM_INFO.getAnyTypes().addAll(anyTypeDAO.findAll().stream().
-                        map(type -> type.getKey()).collect(Collectors.toList()));
+                        map(Entity::getKey).collect(Collectors.toList()));
 
                 PLATFORM_INFO.getUserClasses().clear();
                 PLATFORM_INFO.getUserClasses().addAll(anyTypeDAO.findUser().getClasses().stream().
-                        map(cls -> cls.getKey()).collect(Collectors.toList()));
+                        map(Entity::getKey).collect(Collectors.toList()));
 
                 PLATFORM_INFO.getAnyTypeClasses().clear();
                 PLATFORM_INFO.getAnyTypeClasses().addAll(anyTypeClassDAO.findAll().stream().
-                        map(cls -> cls.getKey()).collect(Collectors.toList()));
+                        map(Entity::getKey).collect(Collectors.toList()));
 
                 PLATFORM_INFO.getResources().clear();
                 PLATFORM_INFO.getResources().addAll(resourceDAO.findAll().stream().
-                        map(resource -> resource.getKey()).collect(Collectors.toList()));
+                        map(Entity::getKey).collect(Collectors.toList()));
                 return null;
             });
         }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
----------------------------------------------------------------------
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
index baad7e8..bcbe645 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
@@ -63,6 +63,7 @@ import org.quartz.SchedulerException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
 
 @Component
 public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
@@ -156,6 +157,7 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
     }
 
     @PreAuthorize("hasRole('" + StandardEntitlement.TASK_LIST + "')")
+    @Transactional(readOnly = true)
     @SuppressWarnings("unchecked")
     public <T extends TaskTO> Pair<Integer, List<T>> list(
             final TaskType type,
@@ -191,6 +193,7 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
     }
 
     @PreAuthorize("hasRole('" + StandardEntitlement.TASK_READ + "')")
+    @Transactional(readOnly = true)
     public <T extends TaskTO> T read(final TaskType type, final String key, final boolean details) {
         Task task = taskDAO.find(key);
         if (task == null) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/logic/src/main/java/org/apache/syncope/core/logic/UserWorkflowLogic.java
----------------------------------------------------------------------
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/UserWorkflowLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/UserWorkflowLogic.java
index 62b43ae..8fc4745 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/UserWorkflowLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/UserWorkflowLogic.java
@@ -58,7 +58,6 @@ public class UserWorkflowLogic extends AbstractTransactionalLogic<WorkflowFormTO
     private UserDAO userDAO;
 
     @PreAuthorize("hasRole('" + StandardEntitlement.WORKFLOW_FORM_CLAIM + "')")
-    @Transactional(rollbackFor = { Throwable.class })
     public WorkflowFormTO claimForm(final String taskId) {
         return uwfAdapter.claimForm(taskId);
     }
@@ -82,20 +81,19 @@ public class UserWorkflowLogic extends AbstractTransactionalLogic<WorkflowFormTO
 
     @PreAuthorize("hasRole('" + StandardEntitlement.WORKFLOW_FORM_READ + "') and hasRole('"
             + StandardEntitlement.USER_READ + "')")
-    @Transactional(rollbackFor = { Throwable.class })
+    @Transactional(readOnly = true)
     public WorkflowFormTO getFormForUser(final String key) {
         User user = userDAO.authFind(key);
         return uwfAdapter.getForm(user.getWorkflowId());
     }
 
     @PreAuthorize("hasRole('" + StandardEntitlement.WORKFLOW_FORM_LIST + "')")
-    @Transactional(rollbackFor = { Throwable.class })
+    @Transactional(readOnly = true)
     public List<WorkflowFormTO> getForms() {
         return uwfAdapter.getForms();
     }
 
     @PreAuthorize("hasRole('" + StandardEntitlement.WORKFLOW_FORM_SUBMIT + "')")
-    @Transactional(rollbackFor = { Throwable.class })
     public UserTO submitForm(final WorkflowFormTO form) {
         WorkflowResult<? extends AnyPatch> updated = uwfAdapter.submitForm(form);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/attrvalue/validation/InvalidEntityException.java
----------------------------------------------------------------------
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/attrvalue/validation/InvalidEntityException.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/attrvalue/validation/InvalidEntityException.java
index f9db494..d6a3fe7 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/attrvalue/validation/InvalidEntityException.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/attrvalue/validation/InvalidEntityException.java
@@ -89,6 +89,8 @@ public class InvalidEntityException extends ValidationException {
 
             entityViolationType.setMessage(message.trim());
 
+            entityViolationType.setPropertyPath(violation.getPropertyPath().toString());
+
             if (!this.violations.containsKey(violation.getLeafBean().getClass())) {
                 this.violations.put(violation.getLeafBean().getClass(), EnumSet.noneOf(EntityViolationType.class));
             }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RemediationDAO.java
----------------------------------------------------------------------
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RemediationDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RemediationDAO.java
new file mode 100644
index 0000000..f00cb8a
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RemediationDAO.java
@@ -0,0 +1,39 @@
+/*
+ * 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.syncope.core.persistence.api.dao;
+
+import java.util.List;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+
+public interface RemediationDAO extends DAO<Remediation> {
+
+    Remediation find(String key);
+
+    List<Remediation> findByPullTask(PullTask pullTask);
+
+    List<Remediation> findAll();
+
+    Remediation save(Remediation remediation);
+
+    void delete(Remediation remediation);
+
+    void delete(String key);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Remediation.java
----------------------------------------------------------------------
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Remediation.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Remediation.java
new file mode 100644
index 0000000..1278241
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/Remediation.java
@@ -0,0 +1,65 @@
+/*
+ * 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.syncope.core.persistence.api.entity;
+
+import java.util.Date;
+import org.apache.syncope.common.lib.patch.AnyPatch;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+
+public interface Remediation extends Entity {
+
+    AnyTypeKind getAnyTypeKind();
+
+    void setAnyTypeKind(AnyTypeKind anyTypeKind);
+
+    ResourceOperation getOperation();
+
+    void setOperation(ResourceOperation operation);
+
+    void setPayload(AnyTO anyTO);
+
+    void setPayload(AnyPatch anyPatch);
+
+    void setPayload(String key);
+
+    <T extends AnyTO> T getPayloadAsTO(Class<T> reference);
+
+    <P extends AnyPatch> P getPayloadAsPatch(Class<P> reference);
+
+    String getPayloadAsKey();
+
+    String getError();
+
+    void setError(String error);
+
+    Date getInstant();
+
+    void setInstant(Date instant);
+
+    PullTask getPullTask();
+
+    void setPullTask(PullTask pullTask);
+
+    String getRemoteName();
+
+    void setRemoteName(String remoteName);
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/PullTask.java
----------------------------------------------------------------------
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/PullTask.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/PullTask.java
index d4234f2..2d8ffdd 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/PullTask.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/task/PullTask.java
@@ -44,4 +44,9 @@ public interface PullTask extends ProvisioningTask {
     Optional<? extends AnyTemplatePullTask> getTemplate(AnyType anyType);
 
     List<? extends AnyTemplatePullTask> getTemplates();
+
+    void setRemediation(boolean remediation);
+
+    boolean isRemediation();
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARemediationDAO.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARemediationDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARemediationDAO.java
new file mode 100644
index 0000000..b78c425
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARemediationDAO.java
@@ -0,0 +1,73 @@
+/*
+ * 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.syncope.core.persistence.jpa.dao;
+
+import java.util.List;
+import javax.persistence.TypedQuery;
+import org.apache.syncope.core.persistence.api.dao.RemediationDAO;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+import org.apache.syncope.core.persistence.jpa.entity.JPARemediation;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public class JPARemediationDAO extends AbstractDAO<Remediation> implements RemediationDAO {
+
+    @Override
+    public Remediation find(final String key) {
+        return entityManager().find(JPARemediation.class, key);
+    }
+
+    @Override
+    public List<Remediation> findByPullTask(final PullTask pullTask) {
+        TypedQuery<Remediation> query = entityManager().createQuery(
+                "SELECT e FROM " + JPARemediation.class.getSimpleName() + " e WHERE e.pullTask=:pullTask",
+                Remediation.class);
+        query.setParameter("pullTask", pullTask);
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Remediation> findAll() {
+        TypedQuery<Remediation> query = entityManager().createQuery(
+                "SELECT e FROM " + JPARemediation.class.getSimpleName() + " e ", Remediation.class);
+        return query.getResultList();
+    }
+
+    @Override
+    public Remediation save(final Remediation remediation) {
+        return entityManager().merge(remediation);
+    }
+
+    @Override
+    public void delete(final Remediation remediation) {
+        entityManager().remove(remediation);
+    }
+
+    @Override
+    public void delete(final String key) {
+        Remediation remediation = find(key);
+        if (remediation == null) {
+            return;
+        }
+
+        delete(remediation);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
index 792a661..d47ba5d 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
@@ -25,6 +25,7 @@ import javax.persistence.TypedQuery;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.TaskType;
+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.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.Entity;
@@ -41,6 +42,7 @@ import org.apache.syncope.core.persistence.jpa.entity.task.JPAPushTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPASchedTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTask;
 import org.apache.syncope.core.persistence.jpa.entity.task.AbstractTask;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.ReflectionUtils;
@@ -48,6 +50,9 @@ import org.springframework.util.ReflectionUtils;
 @Repository
 public class JPATaskDAO extends AbstractDAO<Task> implements TaskDAO {
 
+    @Autowired
+    private RemediationDAO remediationDAO;
+
     @Override
     public Class<? extends Task> getEntityReference(final TaskType type) {
         Class<? extends Task> result = null;
@@ -303,6 +308,12 @@ public class JPATaskDAO extends AbstractDAO<Task> implements TaskDAO {
 
     @Override
     public void delete(final Task task) {
+        if (task instanceof PullTask) {
+            remediationDAO.findByPullTask((PullTask) task).forEach(remediation -> {
+                remediation.setPullTask(null);
+            });
+        }
+
         entityManager().remove(task);
     }
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
index aa2809a..8de177e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAEntityFactory.java
@@ -130,6 +130,7 @@ import org.apache.syncope.core.persistence.api.entity.DynRealm;
 import org.apache.syncope.core.persistence.api.entity.DynRealmMembership;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Privilege;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
 import org.apache.syncope.core.persistence.api.entity.policy.CorrelationRule;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResourceHistoryConf;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnitItem;
@@ -285,6 +286,8 @@ public class JPAEntityFactory implements EntityFactory {
             result = (E) new JPAAccessToken();
         } else if (reference.equals(Implementation.class)) {
             result = (E) new JPAImplementation();
+        } else if (reference.equals(Remediation.class)) {
+            result = (E) new JPARemediation();
         } else {
             throw new IllegalArgumentException("Could not find a JPA implementation of " + reference.getName());
         }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARemediation.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARemediation.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARemediation.java
new file mode 100644
index 0000000..6f31cc9
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPARemediation.java
@@ -0,0 +1,171 @@
+/*
+ * 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.syncope.core.persistence.jpa.entity;
+
+import java.util.Date;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.Lob;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import javax.validation.constraints.NotNull;
+import org.apache.syncope.common.lib.patch.AnyPatch;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+import org.apache.syncope.core.persistence.jpa.entity.task.JPAPullTask;
+import org.apache.syncope.core.persistence.jpa.validation.entity.RemediationCheck;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+
+@Entity
+@Table(name = JPARemediation.TABLE)
+@RemediationCheck
+public class JPARemediation extends AbstractGeneratedKeyEntity implements Remediation {
+
+    private static final long serialVersionUID = -1612530286294448682L;
+
+    public static final String TABLE = "Remediation";
+
+    @NotNull
+    @Enumerated(EnumType.STRING)
+    private AnyTypeKind anyTypeKind;
+
+    @NotNull
+    @Enumerated(EnumType.STRING)
+    private ResourceOperation operation;
+
+    @NotNull
+    @Lob
+    private String payload;
+
+    @NotNull
+    @Lob
+    private String error;
+
+    @NotNull
+    @Temporal(TemporalType.TIMESTAMP)
+    private Date instant;
+
+    @ManyToOne
+    private JPAPullTask pullTask;
+
+    @NotNull
+    private String remoteName;
+
+    @Override
+    public AnyTypeKind getAnyTypeKind() {
+        return anyTypeKind;
+    }
+
+    @Override
+    public void setAnyTypeKind(final AnyTypeKind anyTypeKind) {
+        this.anyTypeKind = anyTypeKind;
+    }
+
+    @Override
+    public ResourceOperation getOperation() {
+        return operation;
+    }
+
+    @Override
+    public void setOperation(final ResourceOperation operation) {
+        this.operation = operation;
+    }
+
+    @Override
+    public <T extends AnyTO> T getPayloadAsTO(final Class<T> reference) {
+        return POJOHelper.deserialize(this.payload, reference);
+    }
+
+    @Override
+    public <P extends AnyPatch> P getPayloadAsPatch(final Class<P> reference) {
+        return POJOHelper.deserialize(this.payload, reference);
+    }
+
+    @Override
+    public String getPayloadAsKey() {
+        return this.payload;
+    }
+
+    @Override
+    public void setPayload(final AnyTO anyTO) {
+        this.payload = POJOHelper.serialize(anyTO);
+    }
+
+    @Override
+    public void setPayload(final AnyPatch anyPatch) {
+        this.payload = POJOHelper.serialize(anyPatch);
+    }
+
+    @Override
+    public void setPayload(final String key) {
+        this.payload = key;
+    }
+
+    @Override
+    public String getError() {
+        return error;
+    }
+
+    @Override
+    public void setError(final String error) {
+        this.error = error;
+    }
+
+    @Override
+    public Date getInstant() {
+        return instant == null
+                ? null
+                : new Date(instant.getTime());
+    }
+
+    @Override
+    public void setInstant(final Date instant) {
+        this.instant = instant == null
+                ? null
+                : new Date(instant.getTime());
+    }
+
+    @Override
+    public PullTask getPullTask() {
+        return pullTask;
+    }
+
+    @Override
+    public void setPullTask(final PullTask pullTask) {
+        checkType(pullTask, JPAPullTask.class);
+        this.pullTask = (JPAPullTask) pullTask;
+    }
+
+    @Override
+    public String getRemoteName() {
+        return remoteName;
+    }
+
+    @Override
+    public void setRemoteName(final String remoteName) {
+        this.remoteName = remoteName;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAPullTask.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAPullTask.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAPullTask.java
index d42c3ea..5974800 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAPullTask.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/task/JPAPullTask.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.jpa.entity.task;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import javax.persistence.Basic;
 import javax.persistence.CascadeType;
 import javax.persistence.DiscriminatorValue;
 import javax.persistence.Entity;
@@ -33,6 +34,8 @@ import javax.persistence.ManyToMany;
 import javax.persistence.ManyToOne;
 import javax.persistence.OneToMany;
 import javax.persistence.OneToOne;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
 import javax.validation.constraints.NotNull;
 import org.apache.syncope.common.lib.types.ImplementationType;
 import org.apache.syncope.common.lib.types.PullMode;
@@ -71,6 +74,11 @@ public class JPAPullTask extends AbstractProvisioningTask implements PullTask {
     @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER, mappedBy = "pullTask")
     private List<JPAAnyTemplatePullTask> templates = new ArrayList<>();
 
+    @Basic
+    @Min(0)
+    @Max(1)
+    private Integer remediation;
+
     @Override
     public PullMode getPullMode() {
         return pullMode;
@@ -134,4 +142,14 @@ public class JPAPullTask extends AbstractProvisioningTask implements PullTask {
         return templates;
     }
 
+    @Override
+    public void setRemediation(final boolean remediation) {
+        this.remediation = getBooleanAsInteger(remediation);
+    }
+
+    @Override
+    public boolean isRemediation() {
+        return isBooleanAsInteger(remediation);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationCheck.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationCheck.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationCheck.java
new file mode 100644
index 0000000..7a0a9c2
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationCheck.java
@@ -0,0 +1,41 @@
+/*
+ * 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.syncope.core.persistence.jpa.validation.entity;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+
+@Target({ ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = RemediationValidator.class)
+@Documented
+public @interface RemediationCheck {
+
+    String message() default "{org.apache.syncope.core.persistence.validation.remediation}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationValidator.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationValidator.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationValidator.java
new file mode 100644
index 0000000..25f9fb2
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/validation/entity/RemediationValidator.java
@@ -0,0 +1,80 @@
+/*
+ * 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.syncope.core.persistence.jpa.validation.entity;
+
+import javax.validation.ConstraintValidatorContext;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.EntityViolationType;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+
+public class RemediationValidator extends AbstractValidator<RemediationCheck, Remediation> {
+
+    @Override
+    public boolean isValid(final Remediation remediation, final ConstraintValidatorContext context) {
+        boolean isValid = true;
+
+        switch (remediation.getOperation()) {
+            case CREATE:
+                if (remediation.getPayloadAsTO(remediation.getAnyTypeKind().getTOClass()) == null) {
+                    context.disableDefaultConstraintViolation();
+                    context.buildConstraintViolationWithTemplate(
+                            getTemplate(EntityViolationType.InvalidRemediation,
+                                    "Expected " + remediation.getAnyTypeKind().getTOClass().getName())).
+                            addPropertyNode("payload").addConstraintViolation();
+
+                    isValid = false;
+                }
+                break;
+
+            case UPDATE:
+                if (remediation.getPayloadAsPatch(remediation.getAnyTypeKind().getPatchClass()) == null) {
+                    context.disableDefaultConstraintViolation();
+                    context.buildConstraintViolationWithTemplate(
+                            getTemplate(EntityViolationType.InvalidRemediation,
+                                    "Expected " + remediation.getAnyTypeKind().getPatchClass().getName())).
+                            addPropertyNode("payload").addConstraintViolation();
+
+                    isValid = false;
+                }
+                break;
+
+            case DELETE:
+                if (!SyncopeConstants.UUID_PATTERN.matcher(remediation.getPayloadAsKey()).matches()) {
+                    context.disableDefaultConstraintViolation();
+                    context.buildConstraintViolationWithTemplate(
+                            getTemplate(EntityViolationType.InvalidRemediation, "Expected UUID")).
+                            addPropertyNode("payload").addConstraintViolation();
+
+                    isValid = false;
+                }
+                break;
+
+            case NONE:
+            default:
+                context.disableDefaultConstraintViolation();
+                context.buildConstraintViolationWithTemplate(
+                        getTemplate(EntityViolationType.InvalidRemediation, "NONE is not allowed")).
+                        addPropertyNode("operation").addConstraintViolation();
+
+                isValid = false;
+        }
+
+        return isValid;
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RemediationTest.java
----------------------------------------------------------------------
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RemediationTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RemediationTest.java
new file mode 100644
index 0000000..02315fa
--- /dev/null
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RemediationTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.syncope.core.persistence.jpa.inner;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.EntityViolationType;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
+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.entity.Remediation;
+import org.apache.syncope.core.persistence.jpa.AbstractTest;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class RemediationTest extends AbstractTest {
+
+    @Autowired
+    private RemediationDAO remediationDAO;
+
+    @Autowired
+    private TaskDAO taskDAO;
+
+    @Test
+    public void findAll() {
+        List<Remediation> remediations = remediationDAO.findAll();
+        assertTrue(remediations.isEmpty());
+    }
+
+    @Test
+    public void create() {
+        Remediation remediation = entityFactory.newEntity(Remediation.class);
+        remediation.setAnyTypeKind(AnyTypeKind.ANY_OBJECT);
+        remediation.setOperation(ResourceOperation.CREATE);
+        remediation.setError("Error");
+        remediation.setInstant(new Date());
+        remediation.setRemoteName("remote");
+        remediation.setPullTask(taskDAO.find("38abbf9e-a1a3-40a1-a15f-7d0ac02f47f1"));
+
+        // missing payload
+        try {
+            remediationDAO.save(remediation);
+            fail("This should not happen");
+        } catch (InvalidEntityException e) {
+            Set<EntityViolationType> violations = e.getViolations().values().iterator().next();
+            assertEquals(2, violations.size());
+            assertTrue(violations.stream().allMatch(violation -> violation.getPropertyPath().equals("payload")));
+        }
+
+        remediation.setPayload(UUID.randomUUID().toString());
+
+        // wrong payload for operation
+        try {
+            remediationDAO.save(remediation);
+            fail("This should not happen");
+        } catch (InvalidEntityException e) {
+            Set<EntityViolationType> violations = e.getViolations().values().iterator().next();
+            assertEquals(1, violations.size());
+            assertTrue(violations.stream().anyMatch(violation -> violation.getPropertyPath().equals("payload")));
+        }
+
+        remediation.setOperation(ResourceOperation.DELETE);
+
+        remediation = remediationDAO.save(remediation);
+        assertNotNull(remediation.getKey());
+        assertNotNull(remediation.getPullTask());
+
+        taskDAO.delete(remediation.getPullTask());
+
+        remediationDAO.flush();
+
+        remediation = remediationDAO.find(remediation.getKey());
+        assertNull(remediation.getPullTask());
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ConnInstanceDataBinder.java
----------------------------------------------------------------------
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ConnInstanceDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ConnInstanceDataBinder.java
index ae7074f..9fa892e 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ConnInstanceDataBinder.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ConnInstanceDataBinder.java
@@ -18,9 +18,11 @@
  */
 package org.apache.syncope.core.provisioning.api.data;
 
+import org.apache.syncope.common.lib.to.ConnInstanceHistoryConfTO;
 import org.apache.syncope.common.lib.to.ConnInstanceTO;
 import org.apache.syncope.common.lib.types.ConnConfPropSchema;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
+import org.apache.syncope.core.persistence.api.entity.ConnInstanceHistoryConf;
 import org.identityconnectors.framework.api.ConfigurationProperty;
 
 public interface ConnInstanceDataBinder {
@@ -31,6 +33,8 @@ public interface ConnInstanceDataBinder {
 
     ConnInstanceTO getConnInstanceTO(ConnInstance connInstance);
 
+    ConnInstanceHistoryConfTO getConnInstanceHistoryConfTO(ConnInstanceHistoryConf history);
+
     ConnInstance update(ConnInstanceTO connInstanceTO);
 
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/RemediationDataBinder.java
----------------------------------------------------------------------
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/RemediationDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/RemediationDataBinder.java
new file mode 100644
index 0000000..42d8332
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/RemediationDataBinder.java
@@ -0,0 +1,28 @@
+/*
+ * 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.syncope.core.provisioning.api.data;
+
+import org.apache.syncope.common.lib.to.RemediationTO;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+
+public interface RemediationDataBinder {
+
+    RemediationTO getRemediationTO(Remediation remediation);
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ResourceDataBinder.java
----------------------------------------------------------------------
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ResourceDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ResourceDataBinder.java
index 9f1ea5d..84b2254 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ResourceDataBinder.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/ResourceDataBinder.java
@@ -18,8 +18,10 @@
  */
 package org.apache.syncope.core.provisioning.api.data;
 
+import org.apache.syncope.common.lib.to.ResourceHistoryConfTO;
 import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResourceHistoryConf;
 
 public interface ResourceDataBinder {
 
@@ -29,4 +31,5 @@ public interface ResourceDataBinder {
 
     ExternalResource update(ExternalResource resource, ResourceTO resourceTO);
 
+    ResourceHistoryConfTO getResourceHistoryConfTO(ExternalResourceHistoryConf history);
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ConnInstanceDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ConnInstanceDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ConnInstanceDataBinderImpl.java
index d4b9206..333311c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ConnInstanceDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ConnInstanceDataBinderImpl.java
@@ -28,6 +28,7 @@ import java.util.List;
 import java.util.Optional;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.ConnInstanceHistoryConfTO;
 import org.apache.syncope.common.lib.to.ConnInstanceTO;
 import org.apache.syncope.common.lib.to.ConnPoolConfTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
@@ -277,4 +278,15 @@ public class ConnInstanceDataBinderImpl implements ConnInstanceDataBinder {
 
         return connInstanceTO;
     }
+
+    @Override
+    public ConnInstanceHistoryConfTO getConnInstanceHistoryConfTO(final ConnInstanceHistoryConf history) {
+        ConnInstanceHistoryConfTO historyTO = new ConnInstanceHistoryConfTO();
+        historyTO.setKey(history.getKey());
+        historyTO.setCreator(history.getCreator());
+        historyTO.setCreation(history.getCreation());
+        historyTO.setConnInstanceTO(history.getConf());
+
+        return historyTO;
+    }
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RemediationDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RemediationDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RemediationDataBinderImpl.java
new file mode 100644
index 0000000..d95f866
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RemediationDataBinderImpl.java
@@ -0,0 +1,65 @@
+/*
+ * 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.syncope.core.provisioning.java.data;
+
+import org.apache.syncope.common.lib.to.RemediationTO;
+import org.apache.syncope.core.persistence.api.entity.Remediation;
+import org.apache.syncope.core.provisioning.api.data.RemediationDataBinder;
+import org.apache.syncope.core.spring.BeanUtils;
+import org.springframework.stereotype.Component;
+
+@Component
+public class RemediationDataBinderImpl implements RemediationDataBinder {
+
+    private static final String[] IGNORE_PROPERTIES = {
+        "payload", "anyTOPayload", "anyPatchPayload", "keyPayload", "pullTask" };
+
+    @Override
+    public RemediationTO getRemediationTO(final Remediation remediation) {
+        RemediationTO remediationTO = new RemediationTO();
+
+        BeanUtils.copyProperties(remediation, remediationTO);
+
+        switch (remediation.getOperation()) {
+            case CREATE:
+                remediationTO.setAnyTOPayload(
+                        remediation.getPayloadAsTO(remediation.getAnyTypeKind().getTOClass()));
+                break;
+
+            case UPDATE:
+                remediationTO.setAnyPatchPayload(
+                        remediation.getPayloadAsPatch(remediation.getAnyTypeKind().getPatchClass()));
+                break;
+
+            case DELETE:
+                remediationTO.setKeyPayload(remediation.getPayloadAsKey());
+                break;
+
+            default:
+        }
+
+        if (remediation.getPullTask() != null) {
+            remediationTO.setPullTask(remediation.getPullTask().getKey());
+            remediationTO.setResource(remediation.getPullTask().getResource().getKey());
+        }
+
+        return remediationTO;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
index 1db8e56..681743e 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
@@ -34,6 +34,7 @@ import org.apache.syncope.common.lib.to.ItemTO;
 import org.apache.syncope.common.lib.to.MappingTO;
 import org.apache.syncope.common.lib.to.OrgUnitTO;
 import org.apache.syncope.common.lib.to.ProvisionTO;
+import org.apache.syncope.common.lib.to.ResourceHistoryConfTO;
 import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.MappingPurpose;
@@ -646,4 +647,15 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
 
         return resourceTO;
     }
+
+    @Override
+    public ResourceHistoryConfTO getResourceHistoryConfTO(final ExternalResourceHistoryConf history) {
+        ResourceHistoryConfTO historyTO = new ResourceHistoryConfTO();
+        historyTO.setKey(history.getKey());
+        historyTO.setCreator(history.getCreator());
+        historyTO.setCreation(history.getCreation());
+        historyTO.setResourceTO(history.getConf());
+
+        return historyTO;
+    }
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
index 1b30e59..6fcc05e 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
@@ -219,6 +219,8 @@ public class TaskDataBinderImpl implements TaskDataBinder {
             // remove all templates not contained in the TO
             pullTask.getTemplates().
                     removeIf(anyTemplate -> !pullTaskTO.getTemplates().containsKey(anyTemplate.getAnyType().getKey()));
+
+            pullTask.setRemediation(pullTaskTO.isRemediation());
         }
 
         // 3. fill the remaining fields
@@ -407,6 +409,8 @@ public class TaskDataBinderImpl implements TaskDataBinder {
                 pullTask.getTemplates().forEach(template -> {
                     pullTaskTO.getTemplates().put(template.getAnyType().getKey(), template.get());
                 });
+
+                pullTaskTO.setRemediation(pullTask.isRemediation());
                 break;
 
             case PUSH:

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
----------------------------------------------------------------------
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 2832cb5..a748562 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
@@ -20,6 +20,7 @@ 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 org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.syncope.common.lib.AnyOperations;
@@ -36,10 +37,13 @@ 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.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.RemediationDAO;
 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.AnyUtils;
+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.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
@@ -78,11 +82,17 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     protected ConnObjectUtils connObjectUtils;
 
     @Autowired
+    protected RemediationDAO remediationDAO;
+
+    @Autowired
     protected VirSchemaDAO virSchemaDAO;
 
     @Autowired
     protected VirAttrCache virAttrCache;
 
+    @Autowired
+    protected EntityFactory entityFactory;
+
     protected SyncopePullExecutor executor;
 
     protected Result latestResult;
@@ -299,6 +309,19 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             LOG.error("Could not create {} {} ", anyTO.getType(), delta.getUid().getUidValue(), e);
             output = e;
             resultStatus = Result.FAILURE;
+
+            if (profile.getTask().isRemediation()) {
+                Remediation entity = entityFactory.newEntity(Remediation.class);
+                entity.setAnyTypeKind(getAnyUtils().getAnyTypeKind());
+                entity.setOperation(ResourceOperation.CREATE);
+                entity.setPayload(anyTO);
+                entity.setError(result.getMessage());
+                entity.setInstant(new Date());
+                entity.setRemoteName(delta.getObject().getName().getNameValue());
+                entity.setPullTask(profile.getTask());
+
+                remediationDAO.save(entity);
+            }
         }
 
         finalize(operation, resultStatus, null, output, delta);
@@ -343,8 +366,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                     resultStatus = Result.FAILURE;
                     output = null;
                 } else {
+                    AnyPatch anyPatch = null;
                     try {
-                        AnyPatch anyPatch = connObjectUtils.getAnyPatch(
+                        anyPatch = connObjectUtils.getAnyPatch(
                                 before.getKey(),
                                 delta.getObject(),
                                 before,
@@ -384,6 +408,19 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                                 provision.getAnyType().getKey(), delta.getUid().getUidValue(), e);
                         output = e;
                         resultStatus = Result.FAILURE;
+
+                        if (profile.getTask().isRemediation()) {
+                            Remediation entity = entityFactory.newEntity(Remediation.class);
+                            entity.setAnyTypeKind(provision.getAnyType().getKind());
+                            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);
+                        }
                     }
                 }
                 finalize(MatchingRule.toEventName(MatchingRule.UPDATE),
@@ -659,6 +696,19 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         result.setMessage(ExceptionUtils.getRootCauseMessage(e));
                         LOG.error("Could not delete {} {}", provision.getAnyType().getKey(), key, e);
                         output = e;
+
+                        if (profile.getTask().isRemediation()) {
+                            Remediation entity = entityFactory.newEntity(Remediation.class);
+                            entity.setAnyTypeKind(provision.getAnyType().getKind());
+                            entity.setOperation(ResourceOperation.DELETE);
+                            entity.setPayload(key);
+                            entity.setError(result.getMessage());
+                            entity.setInstant(new Date());
+                            entity.setRemoteName(delta.getObject().getName().getNameValue());
+                            entity.setPullTask(profile.getTask());
+
+                            remediationDAO.save(entity);
+                        }
                     }
 
                     finalize(ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RemediationServiceImpl.java
----------------------------------------------------------------------
diff --git a/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RemediationServiceImpl.java b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RemediationServiceImpl.java
new file mode 100644
index 0000000..201bb48
--- /dev/null
+++ b/core/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RemediationServiceImpl.java
@@ -0,0 +1,116 @@
+/*
+ * 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.syncope.core.rest.cxf.service;
+
+import java.util.Date;
+import java.util.List;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.patch.AnyPatch;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.RemediationTO;
+import org.apache.syncope.common.rest.api.service.RemediationService;
+import org.apache.syncope.core.logic.RemediationLogic;
+import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class RemediationServiceImpl extends AbstractServiceImpl implements RemediationService {
+
+    @Autowired
+    private RemediationLogic logic;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private GroupDAO groupDAO;
+
+    @Autowired
+    private AnyObjectDAO anyObjectDAO;
+
+    @Override
+    public List<RemediationTO> list() {
+        return logic.list();
+    }
+
+    @Override
+    public RemediationTO read(final String key) {
+        return logic.read(key);
+    }
+
+    @Override
+    public Response delete(final String key) {
+        logic.delete(key);
+        return Response.noContent().build();
+    }
+
+    @Override
+    public Response remedy(final String key, final AnyTO anyTO) {
+        ProvisioningResult<?> created = logic.remedy(key, anyTO, isNullPriorityAsync());
+        return createResponse(created);
+    }
+
+    private void check(final String key, final String anyKey) {
+        RemediationTO remediation = logic.read(key);
+
+        AnyDAO<?> anyDAO;
+        switch (remediation.getAnyTypeKind()) {
+            case USER:
+            default:
+                anyDAO = userDAO;
+                break;
+
+            case GROUP:
+                anyDAO = groupDAO;
+                break;
+
+            case ANY_OBJECT:
+                anyDAO = anyObjectDAO;
+        }
+
+        Date etagDate = anyDAO.findLastChange(anyKey);
+        if (etagDate == null) {
+            throw new NotFoundException(remediation.getAnyTypeKind().name() + " for " + key);
+        }
+        checkETag(String.valueOf(etagDate.getTime()));
+    }
+
+    @Override
+    public Response remedy(final String key, final AnyPatch anyPatch) {
+        check(key, anyPatch.getKey());
+
+        ProvisioningResult<?> updated = logic.remedy(key, anyPatch, isNullPriorityAsync());
+        return modificationResponse(updated);
+    }
+
+    @Override
+    public Response remedy(final String key, final String anyKey) {
+        check(key, anyKey);
+
+        ProvisioningResult<?> deleted = logic.remedy(key, anyKey, isNullPriorityAsync());
+        return modificationResponse(deleted);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
----------------------------------------------------------------------
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 7143545..3151971 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
@@ -86,6 +86,7 @@ import org.apache.syncope.common.rest.api.service.ImplementationService;
 import org.apache.syncope.common.rest.api.service.MailTemplateService;
 import org.apache.syncope.common.rest.api.service.RealmService;
 import org.apache.syncope.common.rest.api.service.RelationshipTypeService;
+import org.apache.syncope.common.rest.api.service.RemediationService;
 import org.apache.syncope.common.rest.api.service.ReportTemplateService;
 import org.apache.syncope.common.rest.api.service.ResourceHistoryService;
 import org.apache.syncope.common.rest.api.service.RoleService;
@@ -241,6 +242,8 @@ public abstract class AbstractITCase {
 
     protected static ImplementationService implementationService;
 
+    protected static RemediationService remediationService;
+
     protected static CamelRouteService camelRouteService;
 
     protected static SAML2SPService saml2SpService;
@@ -311,6 +314,7 @@ public abstract class AbstractITCase {
         schemaService = adminClient.getService(SchemaService.class);
         securityQuestionService = adminClient.getService(SecurityQuestionService.class);
         implementationService = adminClient.getService(ImplementationService.class);
+        remediationService = adminClient.getService(RemediationService.class);
         camelRouteService = adminClient.getService(CamelRouteService.class);
         saml2SpService = adminClient.getService(SAML2SPService.class);
         saml2IdPService = adminClient.getService(SAML2IdPService.class);

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
----------------------------------------------------------------------
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
index 11fbb0b..55c1a85 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
@@ -35,7 +35,6 @@ import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.patch.DeassociationPatch;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.syncope.common.lib.to.ExecTO;
-import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.NotificationTaskTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.UserTO;
@@ -102,29 +101,20 @@ public abstract class AbstractTaskITCase extends AbstractITCase {
      * Clean Syncope and LDAP resource status.
      */
     protected void ldapCleanup() {
-        PagedResult<GroupTO> matchingGroups = groupService.search(new AnyQuery.Builder().realm(
-                SyncopeConstants.ROOT_REALM).
+        groupService.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
                 fiql(SyncopeClient.getGroupSearchConditionBuilder().is("name").equalTo("testLDAPGroup").query()).
-                build());
-        if (matchingGroups.getSize() > 0) {
-            for (GroupTO group : matchingGroups.getResult()) {
-                groupService.deassociate(new DeassociationPatch.Builder().key(group.getKey()).
-                        action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_LDAP).build());
-                groupService.delete(group.getKey());
-            }
-        }
-        PagedResult<UserTO> matchingUsers = userService.search(
-                new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
-                        fiql(SyncopeClient.getUserSearchConditionBuilder().is("username").equalTo("pullFromLDAP").
-                                query()).
-                        build());
-        if (matchingUsers.getSize() > 0) {
-            for (UserTO user : matchingUsers.getResult()) {
-                userService.deassociate(new DeassociationPatch.Builder().key(user.getKey()).
-                        action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_LDAP).build());
-                userService.delete(user.getKey());
-            }
-        }
+                build()).getResult().forEach(group -> {
+                    groupService.deassociate(new DeassociationPatch.Builder().key(group.getKey()).
+                            action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_LDAP).build());
+                    groupService.delete(group.getKey());
+                });
+        userService.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
+                fiql(SyncopeClient.getUserSearchConditionBuilder().is("username").equalTo("pullFromLDAP").query()).
+                build()).getResult().forEach(user -> {
+                    userService.deassociate(new DeassociationPatch.Builder().key(user.getKey()).
+                            action(ResourceDeassociationAction.UNLINK).resource(RESOURCE_NAME_LDAP).build());
+                    userService.delete(user.getKey());
+                });
     }
 
     protected static ExecTO execTask(

http://git-wip-us.apache.org/repos/asf/syncope/blob/51b314fe/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
----------------------------------------------------------------------
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 3e6d67a..6522fcb 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
@@ -35,6 +35,7 @@ import java.util.Date;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Properties;
 import java.util.Set;
 import java.util.UUID;
@@ -65,6 +66,7 @@ import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.ExecTO;
 import org.apache.syncope.common.lib.to.ImplementationTO;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
+import org.apache.syncope.common.lib.to.RemediationTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
@@ -77,6 +79,7 @@ import org.apache.syncope.common.lib.types.PolicyType;
 import org.apache.syncope.common.lib.types.PropagationTaskExecStatus;
 import org.apache.syncope.common.lib.types.ResourceDeassociationAction;
 import org.apache.syncope.common.lib.types.PullMode;
+import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.AnyQuery;
@@ -659,6 +662,87 @@ public class PullTaskITCase extends AbstractTaskITCase {
     }
 
     @Test
+    public void remediation() {
+        // First of all, clear any potential conflict with existing user / group
+        ldapCleanup();
+
+        // 1. create ldap cloned resource, where 'userId' (mandatory on Syncope) is removed from mapping
+        ResourceTO ldap = resourceService.read(RESOURCE_NAME_LDAP);
+        ldap.setKey("ldapForRemediation");
+
+        ProvisionTO provision = ldap.getProvision(AnyTypeKind.USER.name()).get();
+        provision.getVirSchemas().clear();
+        provision.getMapping().getItems().removeIf(item -> "userId".equals(item.getIntAttrName()));
+
+        ldap = createResource(ldap);
+
+        // 2. create PullTask with remediation enabled, for the new resource
+        PullTaskTO pullTask = (PullTaskTO) taskService.list(new TaskQuery.Builder(TaskType.PULL).
+                resource(RESOURCE_NAME_LDAP).build()).getResult().get(0);
+        assertNotNull(pullTask);
+        pullTask.setResource(ldap.getKey());
+        pullTask.setRemediation(true);
+        pullTask.getActions().clear();
+
+        Response response = taskService.create(TaskType.PULL, pullTask);
+        if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) {
+            throw (RuntimeException) clientFactory.getExceptionMapper().fromResponse(response);
+        }
+        pullTask = getObject(response.getLocation(), TaskService.class, PullTaskTO.class);
+        assertNotNull(pullTask);
+
+        try {
+            // 3. execute the pull task and verify that:
+            ExecTO execution = execProvisioningTask(taskService, TaskType.PULL, pullTask.getKey(), 50, false);
+            assertEquals(PropagationTaskExecStatus.SUCCESS, PropagationTaskExecStatus.valueOf(execution.getStatus()));
+
+            // 3a. user was not pulled
+            try {
+                userService.read("pullFromLDAP");
+                fail("This should never happen");
+            } catch (SyncopeClientException e) {
+                assertEquals(ClientExceptionType.NotFound, e.getType());
+            }
+
+            // 3b. remediation was created
+            Optional<RemediationTO> remediation = remediationService.list().stream().
+                    filter(r -> "uid=pullFromLDAP,ou=People,o=isp".equalsIgnoreCase(r.getRemoteName())).
+                    findFirst();
+            assertTrue(remediation.isPresent());
+            assertEquals(AnyTypeKind.USER, remediation.get().getAnyTypeKind());
+            assertEquals(ResourceOperation.CREATE, remediation.get().getOperation());
+            assertNotNull(remediation.get().getAnyTOPayload());
+            assertNull(remediation.get().getAnyPatchPayload());
+            assertNull(remediation.get().getKeyPayload());
+            assertTrue(remediation.get().getError().contains("RequiredValuesMissing [userId]"));
+
+            // 4. remedy by copying the email value to userId
+            UserTO user = (UserTO) remediation.get().getAnyTOPayload();
+            user.getResources().clear();
+
+            String email = user.getPlainAttr("email").get().getValues().get(0);
+            user.getPlainAttrs().add(new AttrTO.Builder().schema("userId").value(email).build());
+
+            remediationService.remedy(remediation.get().getKey(), user);
+
+            // 5. user is now found
+            user = userService.read("pullFromLDAP");
+            assertNotNull(user);
+            assertEquals(email, user.getPlainAttr("userId").get().getValues().get(0));
+
+            // 6. remediation was removed
+            try {
+                remediationService.read(remediation.get().getKey());
+                fail("This should never happen");
+            } catch (SyncopeClientException e) {
+                assertEquals(ClientExceptionType.NotFound, e.getType());
+            }
+        } finally {
+            resourceService.delete(ldap.getKey());
+        }
+    }
+
+    @Test
     public void issueSYNCOPE68() {
         //-----------------------------
         // Create a new user ... it should be updated applying pull policy


Mime
View raw message