You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2019/10/08 09:27:39 UTC

[syncope] branch 2_1_X updated: [SYNCOPE-957] Pull implemented

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

ilgrosso 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 2b7b0f1  [SYNCOPE-957] Pull implemented
2b7b0f1 is described below

commit 2b7b0f194e2bc968c6273b1e4014c1fc2c2b5b5a
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Tue Oct 8 11:26:19 2019 +0200

    [SYNCOPE-957] Pull implemented
---
 .../console/implementations/MyLogicActions.groovy  |  55 ++-
 .../console/implementations/MyPullActions.groovy   |   9 +-
 .../apache/syncope/common/lib/EntityTOUtils.java   |   2 +-
 .../syncope/common/lib/to/LinkedAccountTO.java     |  27 +-
 .../syncope/core/logic/AbstractAnyLogic.java       |  49 +-
 .../syncope/core/logic/init/EntitlementLoader.java |   8 +-
 .../persistence/api/dao/PullCorrelationRule.java   |  33 ++
 .../core/persistence/api/dao/PullMatch.java        | 116 +++++
 .../core/provisioning/api/LogicActions.java        |  25 +-
 .../provisioning/api/PropagationByResource.java    |  20 +-
 .../provisioning/api/cache/VirAttrCacheValue.java  |  69 +--
 .../core/provisioning/api/data/UserDataBinder.java |   4 +
 .../api/pushpull/ProvisioningReport.java           |  30 +-
 .../provisioning/api/pushpull/PullActions.java     |   8 +-
 .../core/provisioning/java/VirAttrHandlerImpl.java |   7 +-
 .../provisioning/java/data/UserDataBinderImpl.java |  48 +-
 .../AbstractPropagationTaskExecutor.java           |   8 +-
 .../PriorityPropagationTaskExecutor.java           |   6 +-
 .../java/propagation/PropagationManagerImpl.java   |  33 +-
 .../java/pushpull/AbstractPullResultHandler.java   | 489 +++++++++----------
 .../pushpull/DefaultRealmPullResultHandler.java    |  43 +-
 .../pushpull/DefaultUserPullResultHandler.java     | 534 ++++++++++++++++++++-
 .../core/provisioning/java/pushpull/PullUtils.java |  95 ++--
 .../core/provisioning/java/pushpull/PushUtils.java |  27 --
 .../provisioning/java/utils/ConnObjectUtils.java   | 124 ++---
 .../camel/CamelUserProvisioningManager.java        |   2 +-
 .../syncope/core/logic/init/OIDCClientLoader.java  |  13 +-
 .../syncope/core/logic/oidc/OIDCUserManager.java   |  29 +-
 .../core/provisioning/api/OIDCProviderActions.java |  18 +-
 .../java/DefaultOIDCProviderActions.java           |  48 --
 .../syncope/core/logic/init/SAML2SPLoader.java     |   2 +-
 .../syncope/core/logic/saml2/SAML2UserManager.java |  29 +-
 .../core/provisioning/api/SAML2IdPActions.java     |  17 +-
 .../buildtools/cxf/DateParamConverterProvider.java |  61 +++
 .../apache/syncope/fit/buildtools/cxf/User.java    |  15 +
 .../syncope/fit/buildtools/cxf/UserMetadata.java   |  41 +-
 .../syncope/fit/buildtools/cxf/UserService.java    |   6 +
 .../fit/buildtools/cxf/UserServiceImpl.java        |  86 ++--
 fit/build-tools/src/main/resources/cxfContext.xml  |   6 +-
 .../fit/core/reference/ITImplementationLookup.java |   1 +
 .../LinkedAccountSamplePullCorrelationRule.java    |  78 +++
 ...LinkedAccountSamplePullCorrelationRuleConf.java |  25 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   3 +
 .../org/apache/syncope/fit/core/BatchITCase.java   |   3 -
 .../apache/syncope/fit/core/DynRealmITCase.java    |   3 -
 .../syncope/fit/core/LinkedAccountITCase.java      | 235 ++++++++-
 .../org/apache/syncope/fit/core/OpenAPIITCase.java |   3 +-
 .../apache/syncope/fit/core/PullTaskITCase.java    |  36 +-
 .../test/resources/DoubleValueLogicActions.groovy  |  62 ++-
 .../src/test/resources/rest/SearchScript.groovy    |   5 +-
 .../src/test/resources/rest/SyncScript.groovy      |  40 +-
 51 files changed, 1944 insertions(+), 792 deletions(-)

diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
index df22aa2..abb2e3e 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
@@ -17,22 +17,69 @@
  * under the License.
  */
 import groovy.transform.CompileStatic
+import java.util.List
+import java.util.function.Function
 import org.apache.syncope.common.lib.patch.AnyPatch
 import org.apache.syncope.common.lib.patch.AttrPatch
 import org.apache.syncope.common.lib.to.AnyTO
 import org.apache.syncope.common.lib.to.AttrTO
+import org.apache.syncope.common.lib.to.PropagationStatus
 import org.apache.syncope.core.provisioning.api.LogicActions
 
 @CompileStatic
 class MyLogicActions implements LogicActions {
   
   @Override
-  <A extends AnyTO> A beforeCreate(final A input) {
-    return input;
+  <A extends AnyTO> Function<A, A> beforeCreate() {
+    Function function = { 
+      A input ->
+      return input;        
+    }
+    return function;
   }
 
   @Override
-  <M extends AnyPatch> M beforeUpdate(final M input) {
-    return input;
+  <A extends AnyTO> Function<A, A> afterCreate(List<PropagationStatus> statuses) {
+    Function function = { 
+      A input ->
+      return input;        
+    }
+    return function;
+  }
+
+  @Override
+  <P extends AnyPatch> Function<P, P> beforeUpdate() {
+    Function function = { 
+      P input ->
+      return input;        
+    }
+    return function;
+  }
+
+  @Override
+  <A extends AnyTO> Function<A, A> afterUpdate(List<PropagationStatus> statuses) {
+    Function function = { 
+      A input ->
+      return input;        
+    }
+    return function;
+  }
+
+  @Override
+  <A extends AnyTO> Function<A, A> beforeDelete() {
+    Function function = { 
+      A input ->
+      return input;        
+    }
+    return function;
+  }
+
+  @Override
+  <A extends AnyTO> Function<A, A> afterDelete(List<PropagationStatus> statuses) {
+    Function function = { 
+      A input ->
+      return input;        
+    }
+    return function;
   }
 }
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPullActions.groovy b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPullActions.groovy
index 0f995f7..96ba05b 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPullActions.groovy
+++ b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPullActions.groovy
@@ -27,13 +27,18 @@ import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions
 import org.identityconnectors.framework.common.objects.SyncDelta
 import org.quartz.JobExecutionException
+import java.util.function.Function
 
 @CompileStatic
 class MyPullActions implements PullActions {
   
   @Override
-  SyncDelta preprocess(ProvisioningProfile profile, SyncDelta delta) {
-    return delta;
+  Function<SyncDelta, SyncDelta> preprocess(ProvisioningProfile<?, ?> profile) {
+    Function function = { 
+      SyncDelta delta ->
+      return delta;        
+    }
+    return function;
   }
   
   @Override
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/EntityTOUtils.java b/common/lib/src/main/java/org/apache/syncope/common/lib/EntityTOUtils.java
index 8aeefc0..ceebdc4 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/EntityTOUtils.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/EntityTOUtils.java
@@ -52,7 +52,7 @@ public final class EntityTOUtils {
             final Collection<LinkedAccountTO> accounts) {
 
         return Collections.unmodifiableMap(accounts.stream().collect(Collectors.toMap(
-                account -> Pair.of(account.getResource(), account.getconnObjectKeyValue()),
+                account -> Pair.of(account.getResource(), account.getConnObjectKeyValue()),
                 Function.identity(),
                 (exist, repl) -> repl)));
     }
diff --git a/common/lib/src/main/java/org/apache/syncope/common/lib/to/LinkedAccountTO.java b/common/lib/src/main/java/org/apache/syncope/common/lib/to/LinkedAccountTO.java
index 967558a..f2f0387 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/to/LinkedAccountTO.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/to/LinkedAccountTO.java
@@ -20,7 +20,6 @@ package org.apache.syncope.common.lib.to;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
-import java.io.Serializable;
 import java.util.HashSet;
 import java.util.Optional;
 import java.util.Set;
@@ -33,7 +32,7 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 @XmlRootElement(name = "linkedAccount")
 @XmlType
-public class LinkedAccountTO implements Serializable {
+public class LinkedAccountTO implements EntityTO {
 
     private static final long serialVersionUID = 7396929732310559535L;
 
@@ -42,8 +41,13 @@ public class LinkedAccountTO implements Serializable {
         private final LinkedAccountTO instance = new LinkedAccountTO();
 
         public Builder(final String resource, final String connObjectKeyValue) {
+            this(null, resource, connObjectKeyValue);
+        }
+
+        public Builder(final String key, final String resource, final String connObjectKeyValue) {
+            instance.setKey(key);
             instance.setResource(resource);
-            instance.setconnObjectKeyValue(connObjectKeyValue);
+            instance.setConnObjectKeyValue(connObjectKeyValue);
         }
 
         public Builder username(final String username) {
@@ -66,6 +70,8 @@ public class LinkedAccountTO implements Serializable {
         }
     }
 
+    private String key;
+
     private String connObjectKeyValue;
 
     private String resource;
@@ -80,11 +86,20 @@ public class LinkedAccountTO implements Serializable {
 
     private final Set<String> privileges = new HashSet<>();
 
-    public String getconnObjectKeyValue() {
+    public String getKey() {
+        return key;
+    }
+
+    @Override
+    public void setKey(final String key) {
+        this.key = key;
+    }
+
+    public String getConnObjectKeyValue() {
         return connObjectKeyValue;
     }
 
-    public void setconnObjectKeyValue(final String connObjectKeyValue) {
+    public void setConnObjectKeyValue(final String connObjectKeyValue) {
         this.connObjectKeyValue = connObjectKeyValue;
     }
 
@@ -142,6 +157,7 @@ public class LinkedAccountTO implements Serializable {
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
+                append(key).
                 append(connObjectKeyValue).
                 append(resource).
                 append(username).
@@ -164,6 +180,7 @@ public class LinkedAccountTO implements Serializable {
         }
         final LinkedAccountTO other = (LinkedAccountTO) obj;
         return new EqualsBuilder().
+                append(key, other.key).
                 append(connObjectKeyValue, other.connObjectKeyValue).
                 append(resource, other.resource).
                 append(username, other.username).
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
index 450fe72..adfa767 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.patch.AnyPatch;
@@ -107,9 +108,10 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
         templateUtils.apply(any, realm.getTemplate(anyType));
 
         List<LogicActions> actions = getActions(realm);
-        for (LogicActions action : actions) {
-            any = action.beforeCreate(any);
-        }
+        any = (TO) actions.stream().
+                map(action -> action.beforeCreate()).
+                reduce(Function.identity(), Function::andThen).
+                apply(any);
 
         LOG.debug("Input: {}\nOutput: {}\n", input, any);
 
@@ -124,16 +126,17 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
             throw sce;
         }
 
-        P mod = input;
+        P patch = input;
 
         List<LogicActions> actions = getActions(realm);
-        for (LogicActions action : actions) {
-            mod = action.beforeUpdate(mod);
-        }
+        patch = (P) actions.stream().
+                map(action -> action.beforeUpdate()).
+                reduce(Function.identity(), Function::andThen).
+                apply(patch);
 
-        LOG.debug("Input: {}\nOutput: {}\n", input, mod);
+        LOG.debug("Input: {}\nOutput: {}\n", input, patch);
 
-        return Pair.of(mod, actions);
+        return Pair.of(patch, actions);
     }
 
     protected Pair<TO, List<LogicActions>> beforeDelete(final TO input) {
@@ -147,9 +150,10 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
         TO any = input;
 
         List<LogicActions> actions = getActions(realm);
-        for (LogicActions action : actions) {
-            any = action.beforeDelete(any);
-        }
+        any = (TO) actions.stream().
+                map(action -> action.beforeDelete()).
+                reduce(Function.identity(), Function::andThen).
+                apply(any);
 
         LOG.debug("Input: {}\nOutput: {}\n", input, any);
 
@@ -161,9 +165,10 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
 
         TO any = input;
 
-        for (LogicActions action : actions) {
-            any = action.afterCreate(any, statuses);
-        }
+        any = (TO) actions.stream().
+                map(action -> action.afterCreate(statuses)).
+                reduce(Function.identity(), Function::andThen).
+                apply(any);
 
         ProvisioningResult<TO> result = new ProvisioningResult<>();
         result.setEntity(any);
@@ -192,9 +197,10 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
 
         TO any = input;
 
-        for (LogicActions action : actions) {
-            any = action.afterUpdate(any, statuses);
-        }
+        any = (TO) actions.stream().
+                map(action -> action.afterUpdate(statuses)).
+                reduce(Function.identity(), Function::andThen).
+                apply(any);
 
         ProvisioningResult<TO> result = new ProvisioningResult<>();
         result.setEntity(any);
@@ -208,9 +214,10 @@ public abstract class AbstractAnyLogic<TO extends AnyTO, P extends AnyPatch> ext
 
         TO any = input;
 
-        for (LogicActions action : actions) {
-            any = action.afterDelete(any, statuses);
-        }
+        any = (TO) actions.stream().
+                map(action -> action.afterDelete(statuses)).
+                reduce(Function.identity(), Function::andThen).
+                apply(any);
 
         ProvisioningResult<TO> result = new ProvisioningResult<>();
         result.setEntity(any);
diff --git a/core/logic/src/main/java/org/apache/syncope/core/logic/init/EntitlementLoader.java b/core/logic/src/main/java/org/apache/syncope/core/logic/init/EntitlementLoader.java
index 6ca7df1..5fa17a5 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/init/EntitlementLoader.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/init/EntitlementLoader.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.core.logic.init;
 
-import java.util.Map;
-import javax.sql.DataSource;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
 import org.apache.syncope.core.provisioning.api.EntitlementsHolder;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
@@ -46,11 +44,11 @@ public class EntitlementLoader implements SyncopeLoader {
     public void load() {
         EntitlementsHolder.getInstance().init(StandardEntitlement.values());
 
-        for (Map.Entry<String, DataSource> entry : domainsHolder.getDomains().entrySet()) {
-            AuthContextUtils.execWithAuthContext(entry.getKey(), () -> {
+        domainsHolder.getDomains().forEach((domain, datasource) -> {
+            AuthContextUtils.execWithAuthContext(domain, () -> {
                 entitlementAccessor.addEntitlementsForAnyTypes();
                 return null;
             });
-        }
+        });
     }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullCorrelationRule.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullCorrelationRule.java
index 24c13e3..a6fe5e7 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullCorrelationRule.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullCorrelationRule.java
@@ -18,8 +18,10 @@
  */
 package org.apache.syncope.core.persistence.api.dao;
 
+import java.util.Optional;
 import org.apache.syncope.common.lib.policy.PullCorrelationRuleConf;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 
@@ -28,6 +30,8 @@ import org.identityconnectors.framework.common.objects.SyncDelta;
  */
 public interface PullCorrelationRule {
 
+    PullMatch NO_MATCH = new PullMatch.Builder().build();
+
     default void setConf(PullCorrelationRuleConf conf) {
     }
 
@@ -39,4 +43,33 @@ public interface PullCorrelationRule {
      * @return search condition.
      */
     SearchCond getSearchCond(SyncDelta syncDelta, Provision provision);
+
+    /**
+     * Create matching information for the given Any, found matching for the given
+     * {@link SyncDelta} and {@link Provision}.
+     * For users, this might end with creating / updating / deleting a
+     * {@link org.apache.syncope.core.persistence.api.entity.user.LinkedAccount}.
+     *
+     * @param any any
+     * @param syncDelta change operation, including external attributes
+     * @param provision resource provision
+     * @return matching information
+     */
+    default PullMatch matching(Any<?> any, SyncDelta syncDelta, Provision provision) {
+        return new PullMatch.Builder().matchingKey(any.getKey()).build();
+    }
+
+    /**
+     * Optionally create matching information in case no matching Any was found for the given
+     * {@link SyncDelta} and {@link Provision}.
+     * For users, this might end with creating a
+     * {@link org.apache.syncope.core.persistence.api.entity.user.LinkedAccount}.
+     *
+     * @param syncDelta change operation, including external attributes
+     * @param provision resource provision
+     * @return matching information
+     */
+    default Optional<PullMatch> unmatching(SyncDelta syncDelta, Provision provision) {
+        return Optional.of(NO_MATCH);
+    }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullMatch.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullMatch.java
new file mode 100644
index 0000000..f66906f
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PullMatch.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.persistence.api.dao;
+
+import java.io.Serializable;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public final class PullMatch implements Serializable {
+
+    private static final long serialVersionUID = 6515473131174179932L;
+
+    public enum MatchTarget {
+        ANY,
+        LINKED_ACCOUNT;
+
+    }
+
+    public static class Builder {
+
+        private final PullMatch instance = new PullMatch();
+
+        public Builder matchingKey(final String matchingKey) {
+            instance.matchingKey = matchingKey;
+            return this;
+        }
+
+        public Builder matchTarget(final MatchTarget matchTarget) {
+            instance.matchTarget = matchTarget;
+            return this;
+        }
+
+        public Builder linkingUserKey(final String linkingUserKey) {
+            instance.linkingUserKey = linkingUserKey;
+            return this;
+        }
+
+        public PullMatch build() {
+            return instance;
+        }
+    }
+
+    private MatchTarget matchTarget = MatchTarget.ANY;
+
+    private String matchingKey;
+
+    private String linkingUserKey;
+
+    private PullMatch() {
+        // private constructor
+    }
+
+    public MatchTarget getMatchTarget() {
+        return matchTarget;
+    }
+
+    public String getMatchingKey() {
+        return matchingKey;
+    }
+
+    public String getLinkingUserKey() {
+        return linkingUserKey;
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(matchTarget).
+                append(matchingKey).
+                append(linkingUserKey).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PullMatch other = (PullMatch) obj;
+        return new EqualsBuilder().
+                append(matchingKey, other.matchingKey).
+                append(matchTarget, other.matchTarget).
+                append(linkingUserKey, other.linkingUserKey).
+                build();
+    }
+
+    @Override
+    public String toString() {
+        return "PullMatch{"
+                + "matchTarget=" + matchTarget
+                + ", matchingKey=" + matchingKey
+                + ", linkingUserKey=" + linkingUserKey + '}';
+    }
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java
index c7f6bc7..1f23a51 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/LogicActions.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.core.provisioning.api;
 
 import java.util.List;
+import java.util.function.Function;
 import org.apache.syncope.common.lib.patch.AnyPatch;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.to.PropagationStatus;
@@ -28,27 +29,27 @@ import org.apache.syncope.common.lib.to.PropagationStatus;
  */
 public interface LogicActions {
 
-    default <A extends AnyTO> A beforeCreate(A input) {
-        return input;
+    default <A extends AnyTO> Function<A, A> beforeCreate() {
+        return Function.identity();
     }
 
-    default <A extends AnyTO> A afterCreate(A input, List<PropagationStatus> statuses) {
-        return input;
+    default <A extends AnyTO> Function<A, A> afterCreate(List<PropagationStatus> statuses) {
+        return Function.identity();
     }
 
-    default <P extends AnyPatch> P beforeUpdate(P input) {
-        return input;
+    default <P extends AnyPatch> Function<P, P> beforeUpdate() {
+        return Function.identity();
     }
 
-    default <A extends AnyTO> A afterUpdate(A input, List<PropagationStatus> statuses) {
-        return input;
+    default <A extends AnyTO> Function<A, A> afterUpdate(List<PropagationStatus> statuses) {
+        return Function.identity();
     }
 
-    default <A extends AnyTO> A beforeDelete(A input) {
-        return input;
+    default <A extends AnyTO> Function<A, A> beforeDelete() {
+        return Function.identity();
     }
 
-    default <A extends AnyTO> A afterDelete(A input, List<PropagationStatus> statuses) {
-        return input;
+    default <A extends AnyTO> Function<A, A> afterDelete(List<PropagationStatus> statuses) {
+        return Function.identity();
     }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
index 5f82ef3..43e71aa 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
@@ -193,28 +193,28 @@ public class PropagationByResource<T extends Serializable> implements Serializab
      * Removes only the resource names in the underlying resource name sets that are contained in the specified
      * collection.
      *
-     * @param resourceKeys collection containing resource names to be retained in the underlying resource name sets
+     * @param keys collection containing resource names to be retained in the underlying resource name sets
      * @return <tt>true</tt> if the underlying resource name sets changed as a result of the call
      * @see Collection#removeAll(java.util.Collection)
      */
-    public boolean removeAll(final Collection<String> resourceKeys) {
-        return toBeCreated.removeAll(resourceKeys)
-                | toBeUpdated.removeAll(resourceKeys)
-                | toBeDeleted.removeAll(resourceKeys);
+    public boolean removeAll(final Collection<T> keys) {
+        return toBeCreated.removeAll(keys)
+                | toBeUpdated.removeAll(keys)
+                | toBeDeleted.removeAll(keys);
     }
 
     /**
      * Retains only the resource names in the underlying resource name sets that are contained in the specified
      * collection.
      *
-     * @param resourceKeys collection containing resource names to be retained in the underlying resource name sets
+     * @param keys collection containing resource names to be retained in the underlying resource name sets
      * @return <tt>true</tt> if the underlying resource name sets changed as a result of the call
      * @see Collection#retainAll(java.util.Collection)
      */
-    public boolean retainAll(final Collection<String> resourceKeys) {
-        return toBeCreated.retainAll(resourceKeys)
-                | toBeUpdated.retainAll(resourceKeys)
-                | toBeDeleted.retainAll(resourceKeys);
+    public boolean retainAll(final Collection<T> keys) {
+        return toBeCreated.retainAll(keys)
+                | toBeUpdated.retainAll(keys)
+                | toBeDeleted.retainAll(keys);
     }
 
     public boolean contains(final ResourceOperation type, final T key) {
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/cache/VirAttrCacheValue.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/cache/VirAttrCacheValue.java
index 83a729b..f68f9aa 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/cache/VirAttrCacheValue.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/cache/VirAttrCacheValue.java
@@ -22,7 +22,8 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
-import java.util.Objects;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 /**
  * Cache entry value.
@@ -32,7 +33,7 @@ public class VirAttrCacheValue {
     /**
      * Virtual attribute values.
      */
-    private final List<String> values;
+    private final List<String> values = new ArrayList<>();
 
     /**
      * Entry creation date.
@@ -44,59 +45,39 @@ public class VirAttrCacheValue {
      */
     private Date lastAccessDate;
 
-    public VirAttrCacheValue() {
-        this.creationDate = new Date();
-        this.lastAccessDate = new Date();
-        this.values = new ArrayList<>();
-    }
-
-    public void setValues(final Collection<Object> values) {
-        this.values.clear();
+    public VirAttrCacheValue(final Collection<Object> values) {
+        creationDate = new Date();
+        lastAccessDate = new Date();
 
         if (values != null) {
-            values.forEach(value -> {
-                this.values.add(value.toString());
-            });
+            values.forEach(value -> this.values.add(value.toString()));
         }
     }
 
+    public List<String> getValues() {
+        lastAccessDate = new Date();
+        return values;
+    }
+
     public Date getCreationDate() {
-        if (creationDate != null) {
-            return new Date(creationDate.getTime());
-        }
-        return null;
+        return new Date(creationDate.getTime());
     }
 
     public void forceExpiring() {
         creationDate = new Date(0);
     }
 
-    public List<String> getValues() {
-        return values;
-    }
-
     public Date getLastAccessDate() {
-        if (lastAccessDate != null) {
-            return new Date(lastAccessDate.getTime());
-        }
-        return null;
-    }
-
-    public void setLastAccessDate(final Date lastAccessDate) {
-        if (lastAccessDate != null) {
-            this.lastAccessDate = new Date(lastAccessDate.getTime());
-        } else {
-            this.lastAccessDate = null;
-        }
+        return new Date(lastAccessDate.getTime());
     }
 
     @Override
     public int hashCode() {
-        int hash = 5;
-        hash = 67 * hash + Objects.hashCode(this.values);
-        hash = 67 * hash + Objects.hashCode(this.creationDate);
-        hash = 67 * hash + Objects.hashCode(this.lastAccessDate);
-        return hash;
+        return new HashCodeBuilder().
+                append(values).
+                append(creationDate).
+                append(lastAccessDate).
+                build();
     }
 
     @Override
@@ -111,13 +92,11 @@ public class VirAttrCacheValue {
             return false;
         }
         final VirAttrCacheValue other = (VirAttrCacheValue) obj;
-        if (!Objects.equals(this.values, other.values)) {
-            return false;
-        }
-        if (!Objects.equals(this.creationDate, other.creationDate)) {
-            return false;
-        }
-        return Objects.equals(this.lastAccessDate, other.lastAccessDate);
+        return new EqualsBuilder().
+                append(values, other.values).
+                append(creationDate, other.creationDate).
+                append(lastAccessDate, other.lastAccessDate).
+                build();
     }
 
     @Override
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/UserDataBinder.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/UserDataBinder.java
index 75bf6a4..61bf2ff 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/UserDataBinder.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/data/UserDataBinder.java
@@ -20,7 +20,9 @@ package org.apache.syncope.core.provisioning.api.data;
 
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 
@@ -34,6 +36,8 @@ public interface UserDataBinder {
 
     UserTO getUserTO(User user, boolean details);
 
+    LinkedAccountTO getLinkedAccountTO(LinkedAccount account);
+
     void create(User user, UserTO userTO, boolean storePassword);
 
     /**
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java
index 4f47eda..3c0a6f0 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java
@@ -104,19 +104,6 @@ public class ProvisioningReport {
         this.uidValue = uidValue;
     }
 
-    @Override
-    public String toString() {
-        return new ToStringBuilder(this).
-                append(message).
-                append(status).
-                append(anyType).
-                append(operation).
-                append(key).
-                append(name).
-                append(uidValue).
-                build();
-    }
-
     /**
      * Human readable report string, using the given trace level.
      *
@@ -148,9 +135,22 @@ public class ProvisioningReport {
      */
     public static String generate(final Collection<ProvisioningReport> results, final TraceLevel level) {
         StringBuilder sb = new StringBuilder();
-        for (ProvisioningReport result : results) {
+        results.forEach(result -> {
             sb.append(result.getReportString(level)).append('\n');
-        }
+        });
         return sb.toString();
     }
+
+    @Override
+    public String toString() {
+        return new ToStringBuilder(this).
+                append(message).
+                append(status).
+                append(anyType).
+                append(operation).
+                append(key).
+                append(name).
+                append(uidValue).
+                build();
+    }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
index d367a7e..2d4d2d5 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import java.util.function.Function;
 import org.apache.syncope.common.lib.patch.AnyPatch;
 import org.apache.syncope.common.lib.to.EntityTO;
 import org.identityconnectors.framework.common.objects.SyncDelta;
@@ -34,11 +35,10 @@ public interface PullActions extends ProvisioningActions {
      * Pre-process the pull information received by the underlying connector, before any internal activity occurs.
      *
      * @param profile profile of the pull being executed.
-     * @param delta retrieved pull information
-     * @return pull information, possibly altered.
+     * @return pull information, possibly altered
      */
-    default SyncDelta preprocess(ProvisioningProfile<?, ?> profile, SyncDelta delta) {
-        return delta;
+    default Function<SyncDelta, SyncDelta> preprocess(ProvisioningProfile<?, ?> profile) {
+        return Function.identity();
     }
 
     /**
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
index 6bc8db3..e3b71d2 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
@@ -126,10 +126,11 @@ public class VirAttrHandlerImpl implements VirAttrHandler {
                         schemasToRead.forEach(schema -> {
                             Attribute attr = connectorObject.getAttributeByName(schema.getExtAttrName());
                             if (attr != null) {
-                                VirAttrCacheValue virAttrCacheValue = new VirAttrCacheValue();
-                                virAttrCacheValue.setValues(attr.getValue());
+                                VirAttrCacheValue virAttrCacheValue = new VirAttrCacheValue(attr.getValue());
                                 virAttrCache.put(
-                                        any.getType().getKey(), any.getKey(), schema.getKey(),
+                                        any.getType().getKey(),
+                                        any.getKey(),
+                                        schema.getKey(),
                                         virAttrCacheValue);
                                 LOG.debug("Values for {} set in cache: {}", schema, virAttrCacheValue);
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
index bc02905..2e22f9d 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
@@ -164,7 +164,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
             LOG.debug("Ignoring invalid resource {}", accountTO.getResource());
         } else {
             Optional<? extends LinkedAccount> found =
-                    user.getLinkedAccount(resource.getKey(), accountTO.getconnObjectKeyValue());
+                    user.getLinkedAccount(resource.getKey(), accountTO.getConnObjectKeyValue());
             LinkedAccount account = found.isPresent()
                     ? found.get()
                     : new Supplier<LinkedAccount>() {
@@ -175,7 +175,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
                             acct.setOwner(user);
                             user.add(acct);
 
-                            acct.setConnObjectKeyValue(accountTO.getconnObjectKeyValue());
+                            acct.setConnObjectKeyValue(accountTO.getConnObjectKeyValue());
                             acct.setResource(resource);
 
                             return acct;
@@ -592,7 +592,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
         userPatch.getLinkedAccounts().stream().filter(patch -> patch.getLinkedAccountTO() != null).forEach(patch -> {
             user.getLinkedAccount(
                     patch.getLinkedAccountTO().getResource(),
-                    patch.getLinkedAccountTO().getconnObjectKeyValue()).ifPresent(account -> {
+                    patch.getLinkedAccountTO().getConnObjectKeyValue()).ifPresent(account -> {
 
                 if (patch.getOperation() == PatchOperation.DELETE) {
                     user.getLinkedAccounts().remove(account);
@@ -694,6 +694,28 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
 
     @Transactional(readOnly = true)
     @Override
+    public LinkedAccountTO getLinkedAccountTO(final LinkedAccount account) {
+        LinkedAccountTO accountTO = new LinkedAccountTO.Builder(
+                account.getKey(), account.getResource().getKey(), account.getConnObjectKeyValue()).
+                username(account.getUsername()).
+                password(account.getPassword()).
+                suspended(BooleanUtils.isTrue(account.isSuspended())).
+                build();
+
+        account.getPlainAttrs().forEach(plainAttr -> {
+            accountTO.getPlainAttrs().add(new AttrTO.Builder().
+                    schema(plainAttr.getSchema().getKey()).
+                    values(plainAttr.getValuesAsStrings()).build());
+        });
+
+        accountTO.getPrivileges().addAll(account.getPrivileges().stream().
+                map(Entity::getKey).collect(Collectors.toList()));
+
+        return accountTO;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
     public UserTO getUserTO(final User user, final boolean details) {
         UserTO userTO = new UserTO();
         userTO.setKey(user.getKey());
@@ -764,25 +786,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
 
             // linked accounts
             userTO.getLinkedAccounts().addAll(
-                    user.getLinkedAccounts().stream().map(account -> {
-                        LinkedAccountTO accountTO = new LinkedAccountTO.Builder(
-                                account.getResource().getKey(), account.getConnObjectKeyValue()).
-                                username(account.getUsername()).
-                                password(user.getPassword()).
-                                suspended(BooleanUtils.isTrue(account.isSuspended())).
-                                build();
-
-                        account.getPlainAttrs().forEach(plainAttr -> {
-                            accountTO.getPlainAttrs().add(new AttrTO.Builder().
-                                    schema(plainAttr.getSchema().getKey()).
-                                    values(plainAttr.getValuesAsStrings()).build());
-                        });
-
-                        accountTO.getPrivileges().addAll(account.getPrivileges().stream().
-                                map(Entity::getKey).collect(Collectors.toList()));
-
-                        return accountTO;
-                    }).collect(Collectors.toList()));
+                    user.getLinkedAccounts().stream().map(this::getLinkedAccountTO).collect(Collectors.toList()));
         }
 
         return userTO;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
index 172f35f..72ef734 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
@@ -662,9 +662,11 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
                 if (attr == null) {
                     virAttrCache.expire(task.getAnyType(), task.getEntityKey(), item.getIntAttrName());
                 } else {
-                    VirAttrCacheValue cacheValue = new VirAttrCacheValue();
-                    cacheValue.setValues(attr.getValue());
-                    virAttrCache.put(task.getAnyType(), task.getEntityKey(), item.getIntAttrName(), cacheValue);
+                    virAttrCache.put(
+                            task.getAnyType(),
+                            task.getEntityKey(),
+                            item.getIntAttrName(),
+                            new VirAttrCacheValue(attr.getValue()));
                 }
             }
         } catch (TimeoutException toe) {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
index bfdab7b..b21dd21 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
@@ -114,15 +114,19 @@ public class PriorityPropagationTaskExecutor extends AbstractPropagationTaskExec
         prioritizedTasks.forEach(task -> {
             TaskExec execution = null;
             ExecStatus execStatus;
+            String errorMessage = null;
             try {
                 execution = newPropagationTaskCallable(task, reporter).call();
                 execStatus = ExecStatus.valueOf(execution.getStatus());
             } catch (Exception e) {
                 LOG.error("Unexpected exception", e);
                 execStatus = ExecStatus.FAILURE;
+                errorMessage = e.getMessage();
             }
             if (execStatus != ExecStatus.SUCCESS) {
-                throw new PropagationException(task.getResource(), execution == null ? null : execution.getMessage());
+                throw new PropagationException(
+                        task.getResource(),
+                        execution == null ? errorMessage : execution.getMessage());
             }
         });
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
index 55fb711..4502529 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
@@ -190,7 +190,14 @@ public class PropagationManagerImpl implements PropagationManager {
         }
 
         if (noPropResourceKeys != null) {
-            propByRes.get(ResourceOperation.CREATE).removeAll(noPropResourceKeys);
+            if (propByRes != null) {
+                propByRes.get(ResourceOperation.CREATE).removeAll(noPropResourceKeys);
+            }
+
+            if (propByLinkedAccount != null) {
+                propByLinkedAccount.get(ResourceOperation.CREATE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+            }
         }
 
         return createTasks(any, password, true, enable, false, propByRes, propByLinkedAccount, vAttrs);
@@ -300,8 +307,19 @@ public class PropagationManagerImpl implements PropagationManager {
             final Collection<AttrTO> vAttrs,
             final Collection<String> noPropResourceKeys) {
 
-        if (noPropResourceKeys != null && propByRes != null) {
-            propByRes.removeAll(noPropResourceKeys);
+        if (noPropResourceKeys != null) {
+            if (propByRes != null) {
+                propByRes.removeAll(noPropResourceKeys);
+            }
+
+            if (propByLinkedAccount != null) {
+                propByLinkedAccount.get(ResourceOperation.CREATE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+                propByLinkedAccount.get(ResourceOperation.UPDATE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+                propByLinkedAccount.get(ResourceOperation.DELETE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+            }
         }
 
         return createTasks(
@@ -354,6 +372,15 @@ public class PropagationManagerImpl implements PropagationManager {
 
         if (noPropResourceKeys != null) {
             localPropByRes.removeAll(noPropResourceKeys);
+
+            if (propByLinkedAccount != null) {
+                propByLinkedAccount.get(ResourceOperation.CREATE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+                propByLinkedAccount.get(ResourceOperation.UPDATE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+                propByLinkedAccount.get(ResourceOperation.DELETE).
+                        removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
+            }
         }
 
         return createTasks(any, null, false, false, true, localPropByRes, propByLinkedAccount, null);
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 997e7f4..7af3282 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
@@ -22,13 +22,13 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import org.apache.commons.lang3.exception.ExceptionUtils;
-import org.apache.commons.lang3.tuple.Pair;
 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.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.MatchingRule;
@@ -46,7 +46,6 @@ 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;
 import org.apache.syncope.core.provisioning.api.AuditManager;
@@ -57,6 +56,7 @@ import org.apache.syncope.core.provisioning.api.notification.NotificationManager
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
 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;
@@ -178,18 +178,22 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         }
     }
 
-    protected List<ProvisioningReport> assign(
-            final SyncDelta delta, final Provision provision, final AnyUtils anyUtils)
-            throws JobExecutionException {
+    protected List<ProvisioningReport> provision(
+            final UnmatchingRule rule,
+            final SyncDelta delta,
+            final Provision provision,
+            final AnyUtils anyUtils) throws JobExecutionException {
 
         if (!profile.getTask().isPerformCreate()) {
             LOG.debug("PullTask not configured for create");
-            finalize(UnmatchingRule.toEventName(UnmatchingRule.ASSIGN), Result.SUCCESS, null, null, delta);
+            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
-        AnyTO anyTO = connObjectUtils.getAnyTO(delta.getObject(), profile.getTask(), provision, anyUtils);
-        anyTO.getResources().add(profile.getTask().getResource().getKey());
+        AnyTO anyTO = connObjectUtils.getAnyTO(delta.getObject(), profile.getTask(), provision, anyUtils, true);
+        if (rule == UnmatchingRule.ASSIGN) {
+            anyTO.getResources().add(profile.getTask().getResource().getKey());
+        }
 
         ProvisioningReport result = new ProvisioningReport();
         result.setOperation(ResourceOperation.CREATE);
@@ -200,46 +204,61 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         if (profile.isDryRun()) {
             result.setKey(null);
-            finalize(UnmatchingRule.toEventName(UnmatchingRule.ASSIGN), Result.SUCCESS, null, null, delta);
+            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
         } else {
             for (PullActions action : profile.getActions()) {
-                action.beforeAssign(profile, delta, anyTO);
+                if (rule == UnmatchingRule.ASSIGN) {
+                    action.beforeAssign(profile, delta, anyTO);
+                } else if (rule == UnmatchingRule.PROVISION) {
+                    action.beforeProvision(profile, delta, anyTO);
+                }
             }
 
-            create(anyTO, delta, UnmatchingRule.toEventName(UnmatchingRule.ASSIGN), provision, result);
-        }
-
-        return Collections.singletonList(result);
-    }
-
-    protected List<ProvisioningReport> provision(
-            final SyncDelta delta, final Provision provision, final AnyUtils anyUtils)
-            throws JobExecutionException {
-
-        if (!profile.getTask().isPerformCreate()) {
-            LOG.debug("PullTask not configured for create");
-            finalize(UnmatchingRule.toEventName(UnmatchingRule.PROVISION), Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
-        }
+            Object output;
+            Result resultStatus;
 
-        AnyTO anyTO = connObjectUtils.getAnyTO(delta.getObject(), profile.getTask(), provision, anyUtils);
+            try {
+                AnyTO created = doCreate(anyTO, delta);
+                output = created;
+                result.setKey(created.getKey());
+                result.setName(getName(created));
+                resultStatus = Result.SUCCESS;
+
+                for (PullActions action : profile.getActions()) {
+                    action.after(profile, delta, created, result);
+                }
 
-        ProvisioningReport result = new ProvisioningReport();
-        result.setOperation(ResourceOperation.CREATE);
-        result.setAnyType(provision.getAnyType().getKey());
-        result.setStatus(ProvisioningReport.Status.SUCCESS);
-        result.setName(getName(anyTO));
-        result.setUidValue(delta.getUid().getUidValue());
+                LOG.debug("{} {} successfully created", created.getType(), created.getKey());
+            } catch (PropagationException e) {
+                // A propagation failure doesn't imply a pull failure.
+                // The propagation exception status will be reported into the propagation task execution.
+                LOG.error("Could not propagate {} {}", anyTO.getType(), delta.getUid().getUidValue(), e);
+                output = e;
+                resultStatus = Result.FAILURE;
+            } catch (Exception e) {
+                throwIgnoreProvisionException(delta, e);
 
-        if (profile.isDryRun()) {
-            result.setKey(null);
-            finalize(UnmatchingRule.toEventName(UnmatchingRule.PROVISION), Result.SUCCESS, null, null, delta);
-        } else {
-            for (PullActions action : profile.getActions()) {
-                action.beforeProvision(profile, delta, anyTO);
+                result.setStatus(ProvisioningReport.Status.FAILURE);
+                result.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                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.setAnyType(provision.getAnyType());
+                    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);
+                }
             }
 
-            create(anyTO, delta, UnmatchingRule.toEventName(UnmatchingRule.PROVISION), provision, result);
+            finalize(UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
         }
 
         return Collections.singletonList(result);
@@ -263,63 +282,10 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         }
     }
 
-    protected void create(
-            final AnyTO anyTO,
-            final SyncDelta delta,
-            final String operation,
-            final Provision provision,
-            final ProvisioningReport result)
-            throws JobExecutionException {
-
-        Object output;
-        Result resultStatus;
-
-        try {
-            AnyTO created = doCreate(anyTO, delta);
-            output = created;
-            result.setKey(created.getKey());
-            result.setName(getName(created));
-            resultStatus = Result.SUCCESS;
-
-            for (PullActions action : profile.getActions()) {
-                action.after(profile, delta, created, result);
-            }
-
-            LOG.debug("{} {} successfully created", created.getType(), created.getKey());
-        } catch (PropagationException e) {
-            // A propagation failure doesn't imply a pull failure.
-            // The propagation exception status will be reported into the propagation task execution.
-            LOG.error("Could not propagate {} {}", anyTO.getType(), delta.getUid().getUidValue(), e);
-            output = e;
-            resultStatus = Result.FAILURE;
-        } catch (Exception e) {
-            throwIgnoreProvisionException(delta, e);
-
-            result.setStatus(ProvisioningReport.Status.FAILURE);
-            result.setMessage(ExceptionUtils.getRootCauseMessage(e));
-            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.setAnyType(provision.getAnyType());
-                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);
-    }
-
     protected List<ProvisioningReport> update(
-            final SyncDelta delta, final List<String> anyKeys, final Provision provision) throws JobExecutionException {
+            final SyncDelta delta,
+            final List<PullMatch> matches,
+            final Provision provision) throws JobExecutionException {
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
@@ -327,23 +293,23 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             return Collections.<ProvisioningReport>emptyList();
         }
 
-        LOG.debug("About to update {}", anyKeys);
+        LOG.debug("About to update {}", matches);
 
         List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : anyKeys) {
-            LOG.debug("About to update {}", key);
+        for (PullMatch match : matches) {
+            LOG.debug("About to update {}", match);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.UPDATE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
+            result.setKey(match.getMatchingKey());
 
-            AnyTO before = getAnyTO(key);
+            AnyTO before = getAnyTO(match.getMatchingKey());
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), key));
+                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), match));
             } else {
                 result.setName(getName(before));
             }
@@ -382,7 +348,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         resultStatus = Result.SUCCESS;
                         result.setName(getName(updated));
 
-                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), key);
+                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), match);
                     } catch (PropagationException e) {
                         // A propagation failure doesn't imply a pull failure.
                         // The propagation exception status will be reported into the propagation task execution.
@@ -423,38 +389,36 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
     }
 
     protected List<ProvisioningReport> deprovision(
+            final MatchingRule matchingRule,
             final SyncDelta delta,
-            final List<String> anyKeys,
-            final Provision provision,
-            final boolean unlink)
+            final List<PullMatch> matches,
+            final Provision provision)
             throws JobExecutionException {
 
         if (!profile.getTask().isPerformUpdate()) {
             LOG.debug("PullTask not configured for update");
-            finalize(unlink
-                    ? MatchingRule.toEventName(MatchingRule.UNASSIGN)
-                    : MatchingRule.toEventName(MatchingRule.DEPROVISION), Result.SUCCESS, null, null, delta);
+            finalize(MatchingRule.toEventName(matchingRule), Result.SUCCESS, null, null, delta);
             return Collections.<ProvisioningReport>emptyList();
         }
 
-        LOG.debug("About to deprovision {}", anyKeys);
+        LOG.debug("About to deprovision {}", matches);
 
-        final List<ProvisioningReport> results = new ArrayList<>();
+        List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : anyKeys) {
-            LOG.debug("About to unassign resource {}", key);
+        for (PullMatch match : matches) {
+            LOG.debug("About to unassign resource {}", match);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.DELETE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
+            result.setKey(match.getMatchingKey());
 
-            AnyTO before = getAnyTO(key);
+            AnyTO before = getAnyTO(match.getMatchingKey());
 
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), key));
+                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), match));
             }
 
             if (!profile.isDryRun()) {
@@ -468,43 +432,36 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                     result.setName(getName(before));
 
                     try {
-                        if (unlink) {
+                        if (matchingRule == MatchingRule.UNASSIGN) {
                             for (PullActions action : profile.getActions()) {
                                 action.beforeUnassign(profile, delta, before);
                             }
-                        } else {
+                        } else if (matchingRule == MatchingRule.DEPROVISION) {
                             for (PullActions action : profile.getActions()) {
                                 action.beforeDeprovision(profile, delta, before);
                             }
                         }
 
                         PropagationByResource<String> propByRes = new PropagationByResource<>();
-                        propByRes.add(ResourceOperation.DELETE, profile.getTask().getResource().getKey());
-
-                        PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
-                        if (getAnyUtils().anyTypeKind() == AnyTypeKind.USER) {
-                            userDAO.findLinkedAccounts(key).forEach(account -> propByLinkedAccount.add(
-                                    ResourceOperation.DELETE,
-                                    Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue())));
-                        }
+                        propByRes.add(ResourceOperation.DELETE, provision.getResource().getKey());
 
                         taskExecutor.execute(propagationManager.getDeleteTasks(
                                 provision.getAnyType().getKind(),
-                                key,
+                                match.getMatchingKey(),
                                 propByRes,
-                                propByLinkedAccount,
+                                null,
                                 null),
                                 false);
 
                         AnyPatch anyPatch = null;
-                        if (unlink) {
-                            anyPatch = getAnyUtils().newAnyPatch(key);
+                        if (matchingRule == MatchingRule.UNASSIGN) {
+                            anyPatch = getAnyUtils().newAnyPatch(match.getMatchingKey());
                             anyPatch.getResources().add(new StringPatchItem.Builder().
                                     operation(PatchOperation.DELETE).
                                     value(profile.getTask().getResource().getKey()).build());
                         }
                         if (anyPatch == null) {
-                            output = getAnyTO(key);
+                            output = getAnyTO(match.getMatchingKey());
                         } else {
                             output = doUpdate(before, anyPatch, delta, result);
                         }
@@ -515,7 +472,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                         resultStatus = Result.SUCCESS;
 
-                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), key);
+                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), match);
                     } catch (PropagationException e) {
                         // A propagation failure doesn't imply a pull failure.
                         // The propagation exception status will be reported into the propagation task execution.
@@ -534,9 +491,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                         resultStatus = Result.FAILURE;
                     }
                 }
-                finalize(unlink
-                        ? MatchingRule.toEventName(MatchingRule.UNASSIGN)
-                        : MatchingRule.toEventName(MatchingRule.DEPROVISION), resultStatus, before, output, delta);
+                finalize(MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
             }
             results.add(result);
         }
@@ -546,7 +501,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
     protected List<ProvisioningReport> link(
             final SyncDelta delta,
-            final List<String> anyKeys,
+            final List<PullMatch> matches,
             final Provision provision,
             final boolean unlink)
             throws JobExecutionException {
@@ -559,24 +514,24 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             return Collections.<ProvisioningReport>emptyList();
         }
 
-        LOG.debug("About to update {}", anyKeys);
+        LOG.debug("About to update {}", matches);
 
         final List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : anyKeys) {
-            LOG.debug("About to unassign resource {}", key);
+        for (PullMatch match : matches) {
+            LOG.debug("About to unassign resource {}", match);
 
             ProvisioningReport result = new ProvisioningReport();
             result.setOperation(ResourceOperation.NONE);
             result.setAnyType(provision.getAnyType().getKey());
             result.setStatus(ProvisioningReport.Status.SUCCESS);
-            result.setKey(key);
+            result.setKey(match.getMatchingKey());
 
-            AnyTO before = getAnyTO(key);
+            AnyTO before = getAnyTO(match.getMatchingKey());
 
             if (before == null) {
                 result.setStatus(ProvisioningReport.Status.FAILURE);
-                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), key));
+                result.setMessage(String.format("Any '%s(%s)' not found", provision.getAnyType().getKey(), match));
             }
 
             if (!profile.isDryRun()) {
@@ -615,7 +570,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                         resultStatus = Result.SUCCESS;
 
-                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), key);
+                        LOG.debug("{} {} successfully updated", provision.getAnyType().getKey(), match);
                     } catch (PropagationException e) {
                         // A propagation failure doesn't imply a pull failure.
                         // The propagation exception status will be reported into the propagation task execution.
@@ -647,7 +602,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
     protected List<ProvisioningReport> delete(
             final SyncDelta delta,
-            final List<String> anyKeys,
+            final List<PullMatch> matches,
             final Provision provision)
             throws JobExecutionException {
 
@@ -657,20 +612,20 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             return Collections.<ProvisioningReport>emptyList();
         }
 
-        LOG.debug("About to delete {}", anyKeys);
+        LOG.debug("About to delete {}", matches);
 
         List<ProvisioningReport> results = new ArrayList<>();
 
-        for (String key : anyKeys) {
+        matches.forEach(match -> {
             Object output;
             Result resultStatus = Result.FAILURE;
 
             ProvisioningReport result = new ProvisioningReport();
 
             try {
-                AnyTO before = getAnyTO(key);
+                AnyTO before = getAnyTO(match.getMatchingKey());
 
-                result.setKey(key);
+                result.setKey(match.getMatchingKey());
                 result.setName(getName(before));
                 result.setOperation(ResourceOperation.DELETE);
                 result.setAnyType(provision.getAnyType().getKey());
@@ -682,8 +637,10 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                     }
 
                     try {
-                        getProvisioningManager().
-                                delete(key, Collections.singleton(profile.getTask().getResource().getKey()), true);
+                        getProvisioningManager().delete(
+                                match.getMatchingKey(),
+                                Collections.singleton(profile.getTask().getResource().getKey()),
+                                true);
                         output = null;
                         resultStatus = Result.SUCCESS;
 
@@ -695,14 +652,14 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                         result.setStatus(ProvisioningReport.Status.FAILURE);
                         result.setMessage(ExceptionUtils.getRootCauseMessage(e));
-                        LOG.error("Could not delete {} {}", provision.getAnyType().getKey(), key, e);
+                        LOG.error("Could not delete {} {}", provision.getAnyType().getKey(), match, e);
                         output = e;
 
                         if (profile.getTask().isRemediation()) {
                             Remediation entity = entityFactory.newEntity(Remediation.class);
                             entity.setAnyType(provision.getAnyType());
                             entity.setOperation(ResourceOperation.DELETE);
-                            entity.setPayload(key);
+                            entity.setPayload(match.getMatchingKey());
                             entity.setError(result.getMessage());
                             entity.setInstant(new Date());
                             entity.setRemoteName(delta.getObject().getName().getNameValue());
@@ -717,20 +674,20 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
                 results.add(result);
             } catch (NotFoundException e) {
-                LOG.error("Could not find {} {}", provision.getAnyType().getKey(), key, e);
+                LOG.error("Could not find {} {}", provision.getAnyType().getKey(), match, e);
             } catch (DelegatedAdministrationException e) {
-                LOG.error("Not allowed to read {} {}", provision.getAnyType().getKey(), key, e);
+                LOG.error("Not allowed to read {} {}", provision.getAnyType().getKey(), match, e);
             } catch (Exception e) {
-                LOG.error("Could not delete {} {}", provision.getAnyType().getKey(), key, e);
+                LOG.error("Could not delete {} {}", provision.getAnyType().getKey(), match, e);
             }
-        }
+        });
 
         return results;
     }
 
     protected List<ProvisioningReport> ignore(
             final SyncDelta delta,
-            final List<String> anyKeys,
+            final List<PullMatch> matches,
             final Provision provision,
             final boolean matching,
             final String... message)
@@ -740,7 +697,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
         List<ProvisioningReport> results = new ArrayList<>();
 
-        if (anyKeys == null) {
+        if (matches == null) {
             ProvisioningReport report = new ProvisioningReport();
             report.setKey(null);
             report.setName(delta.getObject().getUid().getUidValue());
@@ -753,9 +710,9 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
 
             results.add(report);
         } else {
-            for (String anyKey : anyKeys) {
+            matches.forEach(match -> {
                 ProvisioningReport report = new ProvisioningReport();
-                report.setKey(anyKey);
+                report.setKey(match.getMatchingKey());
                 report.setName(delta.getObject().getUid().getUidValue());
                 report.setOperation(ResourceOperation.NONE);
                 report.setAnyType(provision.getAnyType().getKey());
@@ -765,7 +722,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                 }
 
                 results.add(report);
-            }
+            });
         }
 
         finalize(matching
@@ -775,6 +732,100 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         return results;
     }
 
+    protected void handleAnys(
+            final SyncDelta delta,
+            final List<PullMatch> matches,
+            final Provision provision,
+            final AnyUtils anyUtils) throws JobExecutionException {
+
+        if (matches.isEmpty()) {
+            LOG.debug("Nothing to do");
+            return;
+        }
+
+        if (SyncDeltaType.CREATE_OR_UPDATE == delta.getDeltaType()) {
+            if (matches.get(0).getMatchingKey() == null) {
+                switch (profile.getTask().getUnmatchingRule()) {
+                    case ASSIGN:
+                    case PROVISION:
+                        profile.getResults().addAll(
+                                provision(profile.getTask().getUnmatchingRule(), delta, provision, anyUtils));
+                        break;
+
+                    case IGNORE:
+                        profile.getResults().addAll(ignore(delta, null, provision, false));
+                        break;
+
+                    default:
+                    // do nothing
+                    }
+            } else {
+                // update VirAttrCache
+                virSchemaDAO.findByProvision(provision).forEach(virSchema -> {
+                    Attribute attr = delta.getObject().getAttributeByName(virSchema.getExtAttrName());
+                    matches.forEach(match -> {
+                        if (attr == null) {
+                            virAttrCache.expire(
+                                    provision.getAnyType().getKey(),
+                                    match.getMatchingKey(),
+                                    virSchema.getKey());
+                        } else {
+                            virAttrCache.put(
+                                    provision.getAnyType().getKey(),
+                                    match.getMatchingKey(),
+                                    virSchema.getKey(),
+                                    new VirAttrCacheValue(attr.getValue()));
+                        }
+                    });
+                });
+
+                switch (profile.getTask().getMatchingRule()) {
+                    case UPDATE:
+                        profile.getResults().addAll(update(delta, matches, provision));
+                        break;
+
+                    case DEPROVISION:
+                    case UNASSIGN:
+                        profile.getResults().addAll(
+                                deprovision(profile.getTask().getMatchingRule(), delta, matches, provision));
+                        break;
+
+                    case LINK:
+                        profile.getResults().addAll(link(delta, matches, provision, false));
+                        break;
+
+                    case UNLINK:
+                        profile.getResults().addAll(link(delta, matches, provision, true));
+                        break;
+
+                    case IGNORE:
+                        profile.getResults().addAll(ignore(delta, matches, provision, true));
+                        break;
+
+                    default:
+                    // do nothing
+                    }
+            }
+        } else if (SyncDeltaType.DELETE == delta.getDeltaType()) {
+            profile.getResults().addAll(delete(delta, matches, provision));
+        }
+    }
+
+    protected void handleLinkedAccounts(
+            final SyncDelta delta,
+            final List<PullMatch> matches,
+            final Provision provision,
+            final AnyUtils anyUtils) throws JobExecutionException {
+
+        if (matches.isEmpty()) {
+            LOG.debug("Nothing to do");
+            return;
+        }
+
+        // nothing to do in the general case
+        LOG.warn("Unexpected linked accounts found for {}: {}", anyUtils.anyTypeKind(), matches);
+    }
+
     /**
      * Look into SyncDelta and take necessary profile.getActions() (create / update / delete) on any object(s).
      *
@@ -788,115 +839,53 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         LOG.debug("Process {} for {} as {}",
                 delta.getDeltaType(), delta.getUid().getUidValue(), delta.getObject().getObjectClass());
 
-        SyncDelta processed = delta;
-        for (PullActions action : profile.getActions()) {
-            processed = action.preprocess(profile, processed);
-        }
+        SyncDelta finalDelta = profile.getActions().stream().
+                map(action -> action.preprocess(profile)).
+                reduce(Function.identity(), Function::andThen).
+                apply(delta);
 
         LOG.debug("Transformed {} for {} as {}",
-                processed.getDeltaType(), processed.getUid().getUidValue(), processed.getObject().getObjectClass());
+                finalDelta.getDeltaType(), finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass());
 
         try {
-            List<String> keys = pullUtils.match(processed, provision, anyUtils);
+            List<PullMatch> matches = pullUtils.match(finalDelta, provision, anyUtils);
             LOG.debug("Match(es) found for {} as {}: {}",
-                    processed.getUid().getUidValue(), processed.getObject().getObjectClass(), keys);
+                    finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass(), matches);
 
-            if (keys.size() > 1) {
+            if (matches.size() > 1) {
                 switch (profile.getConflictResolutionAction()) {
                     case IGNORE:
                         throw new IgnoreProvisionException("More than one match found for "
-                                + processed.getObject().getUid().getUidValue() + ": " + keys);
+                                + finalDelta.getObject().getUid().getUidValue() + ": " + matches);
 
                     case FIRSTMATCH:
-                        keys = keys.subList(0, 1);
+                        matches = matches.subList(0, 1);
                         break;
 
                     case LASTMATCH:
-                        keys = keys.subList(keys.size() - 1, keys.size());
+                        matches = matches.subList(matches.size() - 1, matches.size());
                         break;
 
                     default:
-                    // keep anyKeys unmodified
+                    // keep matches unmodified
                 }
             }
 
-            if (SyncDeltaType.CREATE_OR_UPDATE == processed.getDeltaType()) {
-                if (keys.isEmpty()) {
-                    switch (profile.getTask().getUnmatchingRule()) {
-                        case ASSIGN:
-                            profile.getResults().addAll(assign(processed, provision, anyUtils));
-                            break;
-
-                        case PROVISION:
-                            profile.getResults().addAll(provision(processed, provision, anyUtils));
-                            break;
-
-                        case IGNORE:
-                            profile.getResults().addAll(ignore(processed, null, provision, false));
-                            break;
-
-                        default:
-                        // do nothing
-                    }
-                } else {
-                    // update VirAttrCache
-                    for (VirSchema virSchema : virSchemaDAO.findByProvision(provision)) {
-                        Attribute attr = processed.getObject().getAttributeByName(virSchema.getExtAttrName());
-                        for (String anyKey : keys) {
-                            if (attr == null) {
-                                virAttrCache.expire(
-                                        provision.getAnyType().getKey(),
-                                        anyKey,
-                                        virSchema.getKey());
-                            } else {
-                                VirAttrCacheValue cacheValue = new VirAttrCacheValue();
-                                cacheValue.setValues(attr.getValue());
-                                virAttrCache.put(
-                                        provision.getAnyType().getKey(),
-                                        anyKey,
-                                        virSchema.getKey(),
-                                        cacheValue);
-                            }
-                        }
-                    }
-
-                    switch (profile.getTask().getMatchingRule()) {
-                        case UPDATE:
-                            profile.getResults().addAll(update(processed, keys, provision));
-                            break;
-
-                        case DEPROVISION:
-                            profile.getResults().addAll(deprovision(processed, keys, provision, false));
-                            break;
-
-                        case UNASSIGN:
-                            profile.getResults().addAll(deprovision(processed, keys, provision, true));
-                            break;
-
-                        case LINK:
-                            profile.getResults().addAll(link(processed, keys, provision, false));
-                            break;
-
-                        case UNLINK:
-                            profile.getResults().addAll(link(processed, keys, provision, true));
-                            break;
-
-                        case IGNORE:
-                            profile.getResults().addAll(ignore(processed, keys, provision, true));
-                            break;
-
-                        default:
-                        // do nothing
-                    }
-                }
-            } else if (SyncDeltaType.DELETE == processed.getDeltaType()) {
-                if (keys.isEmpty()) {
-                    finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, processed);
-                    LOG.debug("No match found for deletion");
-                } else {
-                    profile.getResults().addAll(delete(processed, keys, provision));
-                }
-            }
+            // users, groups and any objects
+            handleAnys(
+                    finalDelta,
+                    matches.stream().
+                            filter(match -> match.getMatchTarget() == PullMatch.MatchTarget.ANY).
+                            collect(Collectors.toList()), provision,
+                    anyUtils);
+
+            // linked accounts
+            handleLinkedAccounts(
+                    finalDelta,
+                    matches.stream().
+                            filter(match -> match.getMatchTarget() == PullMatch.MatchTarget.LINKED_ACCOUNT).
+                            collect(Collectors.toList()), provision,
+                    anyUtils);
         } catch (IllegalStateException | IllegalArgumentException e) {
             LOG.warn(e.getMessage());
         }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
index 0b884dc..3ea2cac 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Function;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.RealmTO;
@@ -670,23 +671,23 @@ public class DefaultRealmPullResultHandler
         LOG.debug("Process {} for {} as {}",
                 delta.getDeltaType(), delta.getUid().getUidValue(), delta.getObject().getObjectClass());
 
-        SyncDelta processed = delta;
-        for (PullActions action : profile.getActions()) {
-            processed = action.preprocess(profile, processed);
-        }
+        SyncDelta finalDelta = profile.getActions().stream().
+                map(action -> action.preprocess(profile)).
+                reduce(Function.identity(), Function::andThen).
+                apply(delta);
 
         LOG.debug("Transformed {} for {} as {}",
-                processed.getDeltaType(), processed.getUid().getUidValue(), processed.getObject().getObjectClass());
+                finalDelta.getDeltaType(), finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass());
 
-        List<String> keys = pullUtils.match(processed, orgUnit);
+        List<String> keys = pullUtils.match(finalDelta, orgUnit);
         LOG.debug("Match found for {} as {}: {}",
-                processed.getUid().getUidValue(), processed.getObject().getObjectClass(), keys);
+                finalDelta.getUid().getUidValue(), finalDelta.getObject().getObjectClass(), keys);
 
         if (keys.size() > 1) {
             switch (profile.getConflictResolutionAction()) {
                 case IGNORE:
                     throw new IgnoreProvisionException("More than one match found for "
-                            + processed.getObject().getUid().getUidValue() + ": " + keys);
+                            + finalDelta.getObject().getUid().getUidValue() + ": " + keys);
 
                 case FIRSTMATCH:
                     keys = keys.subList(0, 1);
@@ -702,19 +703,19 @@ public class DefaultRealmPullResultHandler
         }
 
         try {
-            if (SyncDeltaType.CREATE_OR_UPDATE == processed.getDeltaType()) {
+            if (SyncDeltaType.CREATE_OR_UPDATE == finalDelta.getDeltaType()) {
                 if (keys.isEmpty()) {
                     switch (profile.getTask().getUnmatchingRule()) {
                         case ASSIGN:
-                            profile.getResults().addAll(assign(processed, orgUnit));
+                            profile.getResults().addAll(assign(finalDelta, orgUnit));
                             break;
 
                         case PROVISION:
-                            profile.getResults().addAll(provision(processed, orgUnit));
+                            profile.getResults().addAll(provision(finalDelta, orgUnit));
                             break;
 
                         case IGNORE:
-                            profile.getResults().add(ignore(processed, false));
+                            profile.getResults().add(ignore(finalDelta, false));
                             break;
 
                         default:
@@ -723,39 +724,39 @@ public class DefaultRealmPullResultHandler
                 } else {
                     switch (profile.getTask().getMatchingRule()) {
                         case UPDATE:
-                            profile.getResults().addAll(update(processed, keys, false));
+                            profile.getResults().addAll(update(finalDelta, keys, false));
                             break;
 
                         case DEPROVISION:
-                            profile.getResults().addAll(deprovision(processed, keys, false));
+                            profile.getResults().addAll(deprovision(finalDelta, keys, false));
                             break;
 
                         case UNASSIGN:
-                            profile.getResults().addAll(deprovision(processed, keys, true));
+                            profile.getResults().addAll(deprovision(finalDelta, keys, true));
                             break;
 
                         case LINK:
-                            profile.getResults().addAll(link(processed, keys, false));
+                            profile.getResults().addAll(link(finalDelta, keys, false));
                             break;
 
                         case UNLINK:
-                            profile.getResults().addAll(link(processed, keys, true));
+                            profile.getResults().addAll(link(finalDelta, keys, true));
                             break;
 
                         case IGNORE:
-                            profile.getResults().add(ignore(processed, true));
+                            profile.getResults().add(ignore(finalDelta, true));
                             break;
 
                         default:
                         // do nothing
                     }
                 }
-            } else if (SyncDeltaType.DELETE == processed.getDeltaType()) {
+            } else if (SyncDeltaType.DELETE == finalDelta.getDeltaType()) {
                 if (keys.isEmpty()) {
-                    finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, processed);
+                    finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, finalDelta);
                     LOG.debug("No match found for deletion");
                 } else {
-                    profile.getResults().addAll(delete(processed, keys));
+                    profile.getResults().addAll(delete(finalDelta, keys));
                 }
             }
         } catch (IllegalStateException | IllegalArgumentException e) {
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 953cb26..63b25c4 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
@@ -19,22 +19,46 @@
 package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.util.Collections;
+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;
 import org.apache.syncope.common.lib.patch.AnyPatch;
+import org.apache.syncope.common.lib.patch.LinkedAccountPatch;
 import org.apache.syncope.common.lib.patch.UserPatch;
 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.UserTO;
 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.MatchingRule;
+import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+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.AnyUtils;
+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;
+import org.apache.syncope.core.provisioning.api.PropagationByResource;
 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.core.provisioning.api.pushpull.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.SyncDeltaType;
+import org.quartz.JobExecutionException;
 import org.springframework.beans.factory.annotation.Autowired;
 
 public class DefaultUserPullResultHandler extends AbstractPullResultHandler implements UserPullResultHandler {
@@ -68,14 +92,19 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
         return new WorkflowResult<>(update.getResult().getLeft(), update.getPropByRes(), update.getPerformedTasks());
     }
 
+    public Boolean enabled(final SyncDelta delta) {
+        return profile.getTask().isSyncStatus() ? AttributeUtil.isEnabled(delta.getObject()) : null;
+    }
+
     @Override
     protected AnyTO doCreate(final AnyTO anyTO, final SyncDelta delta) {
-        UserTO userTO = UserTO.class.cast(anyTO);
-
-        Boolean enabled = pullUtils.readEnabled(delta.getObject(), profile.getTask());
-        Map.Entry<String, List<PropagationStatus>> created =
-                userProvisioningManager.create(userTO, true, true, enabled,
-                        Collections.singleton(profile.getTask().getResource().getKey()), true);
+        Map.Entry<String, List<PropagationStatus>> created = userProvisioningManager.create(
+                UserTO.class.cast(anyTO),
+                true,
+                true,
+                enabled(delta),
+                Collections.singleton(profile.getTask().getResource().getKey()),
+                true);
 
         return getAnyTO(created.getKey());
     }
@@ -87,16 +116,499 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
             final SyncDelta delta,
             final ProvisioningReport result) {
 
-        UserPatch userPatch = UserPatch.class.cast(anyPatch);
-        Boolean enabled = pullUtils.readEnabled(delta.getObject(), profile.getTask());
-
         Pair<UserPatch, List<PropagationStatus>> updated = userProvisioningManager.update(
-                userPatch,
+                UserPatch.class.cast(anyPatch),
                 result,
-                enabled,
+                enabled(delta),
                 Collections.singleton(profile.getTask().getResource().getKey()),
                 true);
 
         return updated.getLeft();
     }
+
+    @Override
+    protected void handleLinkedAccounts(
+            final SyncDelta delta,
+            final List<PullMatch> matches,
+            final Provision provision,
+            final AnyUtils anyUtils) throws JobExecutionException {
+
+        for (PullMatch match : matches) {
+            User user = userDAO.find(match.getLinkingUserKey());
+            if (user == null) {
+                LOG.error("Could not find linking user, cannot process match {}", match);
+                return;
+            }
+
+            Optional<? extends LinkedAccount> found =
+                    user.getLinkedAccount(provision.getResource().getKey(), delta.getUid().getUidValue());
+            if (found.isPresent()) {
+                LinkedAccount account = found.get();
+
+                if (SyncDeltaType.CREATE_OR_UPDATE == delta.getDeltaType()) {
+                    switch (profile.getTask().getMatchingRule()) {
+                        case UPDATE:
+                            update(delta, account, provision).ifPresent(profile.getResults()::add);
+                            break;
+
+                        case DEPROVISION:
+                        case UNASSIGN:
+                            deprovision(profile.getTask().getMatchingRule(), delta, account).
+                                    ifPresent(profile.getResults()::add);
+                            break;
+
+                        case LINK:
+                        case UNLINK:
+                            LOG.warn("{} not applicable to linked accounts, ignoring",
+                                    profile.getTask().getMatchingRule());
+                            break;
+
+                        case IGNORE:
+                            profile.getResults().add(ignore(delta, account, true));
+                            break;
+
+                        default:
+                        // do nothing
+                    }
+                } else if (SyncDeltaType.DELETE == delta.getDeltaType()) {
+                    delete(delta, account).ifPresent(profile.getResults()::add);
+                }
+            } else {
+                if (SyncDeltaType.CREATE_OR_UPDATE == delta.getDeltaType()) {
+                    LinkedAccountTO accountTO = new LinkedAccountTO();
+                    accountTO.setConnObjectKeyValue(delta.getUid().getUidValue());
+                    accountTO.setResource(provision.getResource().getKey());
+
+                    switch (profile.getTask().getUnmatchingRule()) {
+                        case ASSIGN:
+                        case PROVISION:
+                            provision(profile.getTask().getUnmatchingRule(), delta, user, accountTO, provision).
+                                    ifPresent(profile.getResults()::add);
+                            break;
+
+                        case IGNORE:
+                            profile.getResults().add(ignore(delta, null, false));
+                            break;
+
+                        default:
+                        // do nothing
+                    }
+                } else if (SyncDeltaType.DELETE == delta.getDeltaType()) {
+                    finalize(
+                            ResourceOperation.DELETE.name().toLowerCase(),
+                            AuditElements.Result.SUCCESS,
+                            null,
+                            null,
+                            delta);
+                    LOG.debug("No match found for deletion");
+                }
+            }
+        }
+    }
+
+    protected Optional<ProvisioningReport> deprovision(
+            final MatchingRule matchingRule,
+            final SyncDelta delta,
+            final LinkedAccount account) throws JobExecutionException {
+
+        if (!profile.getTask().isPerformUpdate()) {
+            LOG.debug("PullTask not configured for update");
+            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
+            return Optional.empty();
+        }
+
+        LOG.debug("About to deprovision {}", account);
+
+        ProvisioningReport report = new ProvisioningReport();
+        report.setOperation(ResourceOperation.DELETE);
+        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setStatus(ProvisioningReport.Status.SUCCESS);
+        report.setKey(account.getKey());
+
+        LinkedAccountTO before = userDataBinder.getLinkedAccountTO(account);
+
+        if (!profile.isDryRun()) {
+            Object output = before;
+            Result resultStatus;
+
+            try {
+                if (matchingRule == MatchingRule.UNASSIGN) {
+                    for (PullActions action : profile.getActions()) {
+                        action.beforeUnassign(profile, delta, before);
+                    }
+                } else if (matchingRule == MatchingRule.DEPROVISION) {
+                    for (PullActions action : profile.getActions()) {
+                        action.beforeDeprovision(profile, delta, before);
+                    }
+                }
+
+                PropagationByResource<Pair<String, String>> propByLinkedAccount = new PropagationByResource<>();
+                propByLinkedAccount.add(
+                        ResourceOperation.DELETE,
+                        Pair.of(account.getResource().getKey(), account.getConnObjectKeyValue()));
+
+                taskExecutor.execute(propagationManager.getDeleteTasks(
+                        AnyTypeKind.USER,
+                        account.getOwner().getKey(),
+                        null,
+                        propByLinkedAccount,
+                        null),
+                        false);
+
+                for (PullActions action : profile.getActions()) {
+                    action.after(profile, delta, before, report);
+                }
+
+                resultStatus = Result.SUCCESS;
+
+                LOG.debug("Linked account {} successfully updated", account.getConnObjectKeyValue());
+            } catch (PropagationException e) {
+                // A propagation failure doesn't imply a pull failure.
+                // The propagation exception status will be reported into the propagation task execution.
+                LOG.error("Could not propagate linked acccount {}", account.getConnObjectKeyValue());
+                output = e;
+                resultStatus = Result.FAILURE;
+            } catch (Exception e) {
+                throwIgnoreProvisionException(delta, e);
+
+                report.setStatus(ProvisioningReport.Status.FAILURE);
+                report.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                LOG.error("Could not update linked account {}", account, e);
+                output = e;
+                resultStatus = Result.FAILURE;
+            }
+
+            finalize(MatchingRule.toEventName(matchingRule), resultStatus, before, output, delta);
+        }
+
+        return Optional.of(report);
+    }
+
+    protected Optional<ProvisioningReport> provision(
+            final UnmatchingRule rule,
+            final SyncDelta delta,
+            final User user,
+            final LinkedAccountTO accountTO,
+            final Provision provision)
+            throws JobExecutionException {
+
+        if (!profile.getTask().isPerformCreate()) {
+            LOG.debug("PullTask not configured for create");
+            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+            return Optional.empty();
+        }
+
+        LOG.debug("About to create {}", accountTO);
+
+        ProvisioningReport report = new ProvisioningReport();
+        report.setOperation(ResourceOperation.CREATE);
+        report.setName(accountTO.getConnObjectKeyValue());
+        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setStatus(ProvisioningReport.Status.SUCCESS);
+
+        if (profile.isDryRun()) {
+            report.setKey(null);
+            finalize(UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
+        } else {
+            UserTO owner = userDataBinder.getUserTO(user, false);
+            UserTO connObject = connObjectUtils.getAnyTO(
+                    delta.getObject(), profile.getTask(), provision, getAnyUtils(), false);
+
+            if (connObject.getUsername().equals(owner.getUsername())) {
+                accountTO.setUsername(null);
+            } else if (!connObject.getUsername().equals(accountTO.getUsername())) {
+                accountTO.setUsername(connObject.getUsername());
+            }
+
+            if (connObject.getPassword() != null) {
+                accountTO.setPassword(connObject.getPassword());
+            }
+
+            accountTO.setSuspended(BooleanUtils.isTrue(BooleanUtils.negate(enabled(delta))));
+
+            connObject.getPlainAttrs().forEach(connObjectAttr -> {
+                Optional<AttrTO> ownerAttr = owner.getPlainAttr(connObjectAttr.getSchema());
+                if (ownerAttr.isPresent() && ownerAttr.get().getValues().equals(connObjectAttr.getValues())) {
+                    accountTO.getPlainAttrs().removeIf(attr -> connObjectAttr.getSchema().equals(attr.getSchema()));
+                } else {
+                    accountTO.getPlainAttrs().add(connObjectAttr);
+                }
+            });
+
+            for (PullActions action : profile.getActions()) {
+                if (rule == UnmatchingRule.ASSIGN) {
+                    action.beforeAssign(profile, delta, accountTO);
+                } else if (rule == UnmatchingRule.PROVISION) {
+                    action.beforeProvision(profile, delta, accountTO);
+                }
+            }
+
+            UserPatch patch = new UserPatch();
+            patch.setKey(user.getKey());
+            patch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                    operation(PatchOperation.ADD_REPLACE).linkedAccountTO(accountTO).build());
+
+            Result resultStatus;
+            Object output;
+
+            try {
+                userProvisioningManager.update(
+                        patch,
+                        report,
+                        null,
+                        Collections.singleton(profile.getTask().getResource().getKey()),
+                        true);
+                resultStatus = Result.SUCCESS;
+
+                LinkedAccountTO created = userDAO.find(patch.getKey()).
+                        getLinkedAccount(accountTO.getResource(), accountTO.getConnObjectKeyValue()).
+                        map(acct -> userDataBinder.getLinkedAccountTO(acct)).
+                        orElse(null);
+                output = created;
+                resultStatus = Result.SUCCESS;
+
+                for (PullActions action : profile.getActions()) {
+                    action.after(profile, delta, created, report);
+                }
+
+                LOG.debug("Linked account {} successfully created", accountTO.getConnObjectKeyValue());
+            } catch (PropagationException e) {
+                // A propagation failure doesn't imply a pull failure.
+                // The propagation exception status will be reported into the propagation task execution.
+                LOG.error("Could not propagate linked acccount {}", accountTO.getConnObjectKeyValue());
+                output = e;
+                resultStatus = Result.FAILURE;
+            } catch (Exception e) {
+                throwIgnoreProvisionException(delta, e);
+
+                report.setStatus(ProvisioningReport.Status.FAILURE);
+                report.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                LOG.error("Could not create linked account {} ", accountTO.getConnObjectKeyValue(), e);
+                output = e;
+                resultStatus = Result.FAILURE;
+            }
+
+            finalize(UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
+        }
+
+        return Optional.of(report);
+    }
+
+    protected Optional<ProvisioningReport> update(
+            final SyncDelta delta,
+            final LinkedAccount account,
+            final Provision provision)
+            throws JobExecutionException {
+
+        if (!profile.getTask().isPerformUpdate()) {
+            LOG.debug("PullTask not configured for update");
+            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
+            return Optional.empty();
+        }
+
+        LOG.debug("About to update {}", account);
+
+        ProvisioningReport report = new ProvisioningReport();
+        report.setOperation(ResourceOperation.UPDATE);
+        report.setKey(account.getKey());
+        report.setName(account.getConnObjectKeyValue());
+        report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+        report.setStatus(ProvisioningReport.Status.SUCCESS);
+
+        if (!profile.isDryRun()) {
+            LinkedAccountTO before = userDataBinder.getLinkedAccountTO(account);
+
+            UserTO owner = userDataBinder.getUserTO(account.getOwner(), false);
+            UserTO connObject = connObjectUtils.getAnyTO(
+                    delta.getObject(), profile.getTask(), provision, getAnyUtils(), false);
+
+            LinkedAccountTO update = userDataBinder.getLinkedAccountTO(account);
+
+            if (connObject.getUsername().equals(owner.getUsername())) {
+                update.setUsername(null);
+            } else if (!connObject.getUsername().equals(update.getUsername())) {
+                update.setUsername(connObject.getUsername());
+            }
+
+            if (connObject.getPassword() != null) {
+                update.setPassword(connObject.getPassword());
+            }
+
+            update.setSuspended(BooleanUtils.isTrue(BooleanUtils.negate(enabled(delta))));
+
+            Set<String> attrsToRemove = new HashSet<>();
+            connObject.getPlainAttrs().forEach(connObjectAttr -> {
+                Optional<AttrTO> ownerAttr = owner.getPlainAttr(connObjectAttr.getSchema());
+                if (ownerAttr.isPresent() && ownerAttr.get().getValues().equals(connObjectAttr.getValues())) {
+                    attrsToRemove.add(connObjectAttr.getSchema());
+                } else {
+                    Optional<AttrTO> updateAttr = update.getPlainAttr(connObjectAttr.getSchema());
+                    if (!updateAttr.isPresent() || !updateAttr.get().getValues().equals(connObjectAttr.getValues())) {
+                        attrsToRemove.add(connObjectAttr.getSchema());
+                        update.getPlainAttrs().add(connObjectAttr);
+                    }
+                }
+            });
+            update.getPlainAttrs().removeIf(attr -> attrsToRemove.contains(attr.getSchema()));
+
+            UserPatch patch = new UserPatch();
+            patch.setKey(account.getOwner().getKey());
+            patch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                    operation(PatchOperation.ADD_REPLACE).linkedAccountTO(update).build());
+
+            for (PullActions action : profile.getActions()) {
+                action.beforeUpdate(profile, delta, before, patch);
+            }
+
+            Result resultStatus;
+            Object output;
+
+            try {
+                userProvisioningManager.update(
+                        patch,
+                        report,
+                        null,
+                        Collections.singleton(profile.getTask().getResource().getKey()),
+                        true);
+                resultStatus = Result.SUCCESS;
+
+                LinkedAccountTO updated = userDAO.find(patch.getKey()).
+                        getLinkedAccount(account.getResource().getKey(), account.getConnObjectKeyValue()).
+                        map(acct -> userDataBinder.getLinkedAccountTO(acct)).
+                        orElse(null);
+                output = updated;
+                resultStatus = Result.SUCCESS;
+
+                for (PullActions action : profile.getActions()) {
+                    action.after(profile, delta, updated, report);
+                }
+
+                LOG.debug("Linked account {} successfully updated", account.getConnObjectKeyValue());
+            } catch (PropagationException e) {
+                // A propagation failure doesn't imply a pull failure.
+                // The propagation exception status will be reported into the propagation task execution.
+                LOG.error("Could not propagate linked acccount {}", account.getConnObjectKeyValue());
+                output = e;
+                resultStatus = Result.FAILURE;
+            } catch (Exception e) {
+                throwIgnoreProvisionException(delta, e);
+
+                report.setStatus(ProvisioningReport.Status.FAILURE);
+                report.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                LOG.error("Could not update linked account {}", account, e);
+                output = e;
+                resultStatus = Result.FAILURE;
+            }
+
+            finalize(MatchingRule.toEventName(MatchingRule.UPDATE), resultStatus, before, output, delta);
+        }
+
+        return Optional.of(report);
+    }
+
+    protected Optional<ProvisioningReport> delete(
+            final SyncDelta delta,
+            final LinkedAccount account)
+            throws JobExecutionException {
+
+        if (!profile.getTask().isPerformDelete()) {
+            LOG.debug("PullTask not configured for delete");
+            finalize(ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
+            return Optional.empty();
+        }
+
+        LOG.debug("About to delete {}", account);
+
+        Object output;
+        Result resultStatus = Result.FAILURE;
+
+        ProvisioningReport report = new ProvisioningReport();
+
+        try {
+            report.setKey(account.getKey());
+            report.setName(account.getConnObjectKeyValue());
+            report.setOperation(ResourceOperation.DELETE);
+            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setStatus(ProvisioningReport.Status.SUCCESS);
+
+            if (!profile.isDryRun()) {
+                LinkedAccountTO before = userDataBinder.getLinkedAccountTO(account);
+
+                for (PullActions action : profile.getActions()) {
+                    action.beforeDelete(profile, delta, before);
+                }
+
+                UserPatch patch = new UserPatch();
+                patch.setKey(account.getOwner().getKey());
+                patch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                        operation(PatchOperation.DELETE).linkedAccountTO(before).build());
+
+                try {
+                    userProvisioningManager.update(
+                            patch,
+                            report,
+                            null,
+                            Collections.singleton(profile.getTask().getResource().getKey()),
+                            true);
+                    resultStatus = Result.SUCCESS;
+
+                    output = null;
+
+                    for (PullActions action : profile.getActions()) {
+                        action.after(profile, delta, before, report);
+                    }
+                } catch (Exception e) {
+                    throwIgnoreProvisionException(delta, e);
+
+                    report.setStatus(ProvisioningReport.Status.FAILURE);
+                    report.setMessage(ExceptionUtils.getRootCauseMessage(e));
+                    LOG.error("Could not delete linked account {}", account, e);
+                    output = e;
+                }
+
+                finalize(ResourceOperation.DELETE.name().toLowerCase(), resultStatus, before, output, delta);
+            }
+        } catch (Exception e) {
+            LOG.error("Could not delete linked account {}", account, e);
+        }
+
+        return Optional.of(report);
+    }
+
+    protected ProvisioningReport ignore(
+            final SyncDelta delta,
+            final LinkedAccount account,
+            final boolean matching,
+            final String... message)
+            throws JobExecutionException {
+
+        LOG.debug("Linked account to ignore {}", delta.getObject().getUid().getUidValue());
+
+        ProvisioningReport report = new ProvisioningReport();
+        if (account == null) {
+            report.setKey(null);
+            report.setName(delta.getObject().getUid().getUidValue());
+            report.setOperation(ResourceOperation.NONE);
+            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setStatus(ProvisioningReport.Status.SUCCESS);
+            if (message != null && message.length >= 1) {
+                report.setMessage(message[0]);
+            }
+        } else {
+            report.setKey(account.getKey());
+            report.setName(delta.getObject().getUid().getUidValue());
+            report.setOperation(ResourceOperation.NONE);
+            report.setAnyType(PullMatch.MatchTarget.LINKED_ACCOUNT.name());
+            report.setStatus(ProvisioningReport.Status.SUCCESS);
+            if (message != null && message.length >= 1) {
+                report.setMessage(message[0]);
+            }
+        }
+
+        finalize(matching
+                ? MatchingRule.toEventName(MatchingRule.IGNORE)
+                : UnmatchingRule.toEventName(UnmatchingRule.IGNORE), AuditElements.Result.SUCCESS, null, null, delta);
+
+        return report;
+    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullUtils.java
index 2040705..4c4bfbe 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PullUtils.java
@@ -53,11 +53,11 @@ import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnitItem;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.persistence.api.entity.task.ProvisioningTask;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
@@ -65,7 +65,6 @@ import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.AttributeUtil;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
 import org.identityconnectors.framework.common.objects.Name;
-import org.identityconnectors.framework.common.objects.OperationalAttributes;
 import org.identityconnectors.framework.common.objects.SearchResult;
 import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
 import org.identityconnectors.framework.common.objects.SyncDelta;
@@ -162,21 +161,21 @@ public class PullUtils {
 
             ConnectorObject connObj = found.iterator().next();
             try {
-                List<String> anyKeys = match(
+                List<PullMatch> matches = match(
                         new SyncDeltaBuilder().
                                 setToken(new SyncToken("")).
                                 setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
                                 setObject(connObj).
                                 build(),
                         provision.get(), anyUtils);
-                if (anyKeys.isEmpty()) {
+                if (matches.isEmpty()) {
                     LOG.debug("No matching {} found for {}, aborting", anyUtils.anyTypeKind(), connObj);
                 } else {
-                    if (anyKeys.size() > 1) {
-                        LOG.warn("More than one {} found {} - taking first only", anyUtils.anyTypeKind(), anyKeys);
+                    if (matches.size() > 1) {
+                        LOG.warn("More than one {} found {} - taking first only", anyUtils.anyTypeKind(), matches);
                     }
 
-                    result = Optional.ofNullable(anyKeys.iterator().next());
+                    result = Optional.ofNullable(matches.iterator().next().getMatchingKey());
                 }
             } catch (IllegalArgumentException e) {
                 LOG.warn(e.getMessage());
@@ -186,9 +185,11 @@ public class PullUtils {
         return result;
     }
 
-    private List<String> findByConnObjectKey(
+    private List<PullMatch> findByConnObjectKey(
             final SyncDelta syncDelta, final Provision provision, final AnyUtils anyUtils) {
 
+        List<PullMatch> noMatchResult = Collections.singletonList(PullCorrelationRule.NO_MATCH);
+
         String connObjectKey = null;
 
         Optional<? extends MappingItem> connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
@@ -200,7 +201,7 @@ public class PullUtils {
             }
         }
         if (connObjectKey == null) {
-            return Collections.emptyList();
+            return noMatchResult;
         }
 
         for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem.get())) {
@@ -213,8 +214,6 @@ public class PullUtils {
             }
         }
 
-        List<String> result = new ArrayList<>();
-
         IntAttrName intAttrName;
         try {
             intAttrName = intAttrNameParser.parse(
@@ -222,15 +221,17 @@ public class PullUtils {
                     provision.getAnyType().getKind());
         } catch (ParseException e) {
             LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.get().getIntAttrName(), e);
-            return result;
+            return noMatchResult;
         }
 
+        List<PullMatch> result = new ArrayList<>();
+
         if (intAttrName.getField() != null) {
             switch (intAttrName.getField()) {
                 case "key":
                     Any<?> any = anyUtils.dao().find(connObjectKey);
                     if (any != null) {
-                        result.add(any.getKey());
+                        result.add(new PullMatch.Builder().matchingKey(any.getKey()).build());
                     }
                     break;
 
@@ -239,12 +240,13 @@ public class PullUtils {
                         AnyCond cond = new AnyCond(AttributeCond.Type.IEQ);
                         cond.setSchema("username");
                         cond.setExpression(connObjectKey);
-                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.USER).
-                                stream().map(Entity::getKey).collect(Collectors.toList()));
+                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.USER).stream().
+                                map(user -> new PullMatch.Builder().matchingKey(user.getKey()).build()).
+                                collect(Collectors.toList()));
                     } else {
                         User user = userDAO.findByUsername(connObjectKey);
                         if (user != null) {
-                            result.add(user.getKey());
+                            result.add(new PullMatch.Builder().matchingKey(user.getKey()).build());
                         }
                     }
                     break;
@@ -254,12 +256,13 @@ public class PullUtils {
                         AnyCond cond = new AnyCond(AttributeCond.Type.IEQ);
                         cond.setSchema("name");
                         cond.setExpression(connObjectKey);
-                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.GROUP).
-                                stream().map(Entity::getKey).collect(Collectors.toList()));
+                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.GROUP).stream().
+                                map(group -> new PullMatch.Builder().matchingKey(group.getKey()).build()).
+                                collect(Collectors.toList()));
                     } else {
                         Group group = groupDAO.findByName(connObjectKey);
                         if (group != null) {
-                            result.add(group.getKey());
+                            result.add(new PullMatch.Builder().matchingKey(group.getKey()).build());
                         }
                     }
 
@@ -267,12 +270,13 @@ public class PullUtils {
                         AnyCond cond = new AnyCond(AttributeCond.Type.IEQ);
                         cond.setSchema("name");
                         cond.setExpression(connObjectKey);
-                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.ANY_OBJECT).
-                                stream().map(Entity::getKey).collect(Collectors.toList()));
+                        result.addAll(searchDAO.search(SearchCond.getLeafCond(cond), AnyTypeKind.ANY_OBJECT).stream().
+                                map(anyObject -> new PullMatch.Builder().matchingKey(anyObject.getKey()).build()).
+                                collect(Collectors.toList()));
                     } else {
                         AnyObject anyObject = anyObjectDAO.findByName(connObjectKey);
                         if (anyObject != null) {
-                            result.add(anyObject.getKey());
+                            result.add(new PullMatch.Builder().matchingKey(anyObject.getKey()).build());
                         }
                     }
                     break;
@@ -295,35 +299,46 @@ public class PullUtils {
                     if (intAttrName.getSchema().isUniqueConstraint()) {
                         anyUtils.dao().findByPlainAttrUniqueValue((PlainSchema) intAttrName.getSchema(),
                                 (PlainAttrUniqueValue) value, provision.isIgnoreCaseMatch()).
-                                ifPresent(found -> result.add(found.getKey()));
+                                ifPresent(any -> result.add(new PullMatch.Builder().matchingKey(any.getKey()).build()));
                     } else {
                         result.addAll(anyUtils.dao().findByPlainAttrValue((PlainSchema) intAttrName.getSchema(),
-                                value, provision.isIgnoreCaseMatch()).
-                                stream().map(Entity::getKey).collect(Collectors.toList()));
+                                value, provision.isIgnoreCaseMatch()).stream().
+                                map(any -> new PullMatch.Builder().matchingKey(any.getKey()).build()).
+                                collect(Collectors.toList()));
                     }
                     break;
 
                 case DERIVED:
-                    result.addAll(anyUtils.dao().findByDerAttrValue(
-                            (DerSchema) intAttrName.getSchema(), connObjectKey, provision.isIgnoreCaseMatch()).
-                            stream().map(Entity::getKey).collect(Collectors.toList()));
+                    result.addAll(anyUtils.dao().findByDerAttrValue((DerSchema) intAttrName.getSchema(),
+                            connObjectKey, provision.isIgnoreCaseMatch()).stream().
+                            map(any -> new PullMatch.Builder().matchingKey(any.getKey()).build()).
+                            collect(Collectors.toList()));
                     break;
 
                 default:
             }
         }
 
-        return result;
+        return result.isEmpty() ? noMatchResult : result;
     }
 
-    private List<String> findByCorrelationRule(
+    private List<PullMatch> findByCorrelationRule(
             final SyncDelta syncDelta,
             final Provision provision,
             final PullCorrelationRule rule,
             final AnyTypeKind type) {
 
-        return searchDAO.search(rule.getSearchCond(syncDelta, provision), type).stream().
-                map(Entity::getKey).collect(Collectors.toList());
+        List<PullMatch> result = new ArrayList<>();
+
+        result.addAll(searchDAO.search(rule.getSearchCond(syncDelta, provision), type).stream().
+                map(any -> rule.matching(any, syncDelta, provision)).
+                collect(Collectors.toList()));
+
+        if (result.isEmpty()) {
+            rule.unmatching(syncDelta, provision).ifPresent(result::add);
+        }
+
+        return result;
     }
 
     /**
@@ -334,7 +349,7 @@ public class PullUtils {
      * @param anyUtils any utils
      * @return list of matching users' / groups' / any objects' keys
      */
-    public List<String> match(
+    public List<PullMatch> match(
             final SyncDelta syncDelta,
             final Provision provision,
             final AnyUtils anyUtils) {
@@ -358,7 +373,7 @@ public class PullUtils {
                     : findByConnObjectKey(syncDelta, provision, anyUtils);
         } catch (RuntimeException e) {
             LOG.error("Could not match {} with any existing {}", syncDelta, provision.getAnyType(), e);
-            return Collections.<String>emptyList();
+            return Collections.emptyList();
         }
     }
 
@@ -432,16 +447,4 @@ public class PullUtils {
 
         return result;
     }
-
-    public Boolean readEnabled(final ConnectorObject connectorObject, final ProvisioningTask task) {
-        Boolean enabled = null;
-        if (task.isSyncStatus()) {
-            Attribute status = AttributeUtil.find(OperationalAttributes.ENABLE_NAME, connectorObject.getAttributes());
-            if (status != null && status.getValue() != null && !status.getValue().isEmpty()) {
-                enabled = (Boolean) status.getValue().get(0);
-            }
-        }
-
-        return enabled;
-    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushUtils.java
index 8594401..b260fb5 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushUtils.java
@@ -27,7 +27,6 @@ import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.policy.PushCorrelationRuleEntity;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 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.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.TimeoutException;
@@ -35,7 +34,6 @@ import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
-import org.identityconnectors.framework.common.objects.Name;
 import org.identityconnectors.framework.common.objects.SearchResult;
 import org.identityconnectors.framework.spi.SearchResultsHandler;
 import org.slf4j.Logger;
@@ -139,29 +137,4 @@ public class PushUtils {
 
         return obj == null ? Collections.emptyList() : Collections.singletonList(obj);
     }
-
-    public ConnectorObject match(
-            final Connector connector,
-            final LinkedAccount account,
-            final Provision provision) {
-
-        Optional<? extends MappingItem> connObjectKey = MappingUtils.getConnObjectKeyItem(provision);
-        String connObjectKeyName = connObjectKey.isPresent()
-                ? connObjectKey.get().getExtAttrName()
-                : Name.NAME;
-
-        ConnectorObject obj = null;
-        try {
-            obj = connector.getObject(
-                    provision.getObjectClass(),
-                    AttributeBuilder.build(connObjectKeyName, account.getConnObjectKeyValue()),
-                    provision.isIgnoreCaseMatch(),
-                    MappingUtils.buildOperationOptions(provision.getMapping().getItems().iterator()));
-        } catch (TimeoutException toe) {
-            LOG.debug("Request timeout", toe);
-            throw toe;
-        }
-
-        return obj;
-    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
index 4f2d52a..4f1f5e9 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
@@ -154,6 +154,8 @@ public class ConnObjectUtils {
      * @param pullTask pull task
      * @param provision provision information
      * @param anyUtils utils
+     * @param generatePasswordIfPossible whether password value shall be generated, in case not found from
+     * connector object and allowed by resource configuration
      * @param <T> any object
      * @return UserTO for the user to be created
      */
@@ -162,14 +164,15 @@ public class ConnObjectUtils {
             final ConnectorObject obj,
             final PullTask pullTask,
             final Provision provision,
-            final AnyUtils anyUtils) {
+            final AnyUtils anyUtils,
+            final boolean generatePasswordIfPossible) {
 
         T anyTO = getAnyTOFromConnObject(obj, pullTask, provision, anyUtils);
 
         // (for users) if password was not set above, generate if resource is configured for that
         if (anyTO instanceof UserTO
                 && StringUtils.isBlank(((UserTO) anyTO).getPassword())
-                && provision.getResource().isRandomPwdIfNotProvided()) {
+                && generatePasswordIfPossible && provision.getResource().isRandomPwdIfNotProvided()) {
 
             UserTO userTO = (UserTO) anyTO;
 
@@ -187,9 +190,7 @@ public class ConnObjectUtils {
             userTO.getResources().stream().
                     map(resource -> resourceDAO.find(resource)).
                     filter(resource -> resource != null && resource.getPasswordPolicy() != null).
-                    forEach(resource -> {
-                        passwordPolicies.add(resource.getPasswordPolicy());
-                    });
+                    forEach(resource -> passwordPolicies.add(resource.getPasswordPolicy()));
 
             String password;
             try {
@@ -241,64 +242,63 @@ public class ConnObjectUtils {
         updated.setKey(key);
 
         T anyPatch = null;
-        if (null != anyUtils.anyTypeKind()) {
-            switch (anyUtils.anyTypeKind()) {
-                case USER:
-                    UserTO originalUser = (UserTO) original;
-                    UserTO updatedUser = (UserTO) updated;
-
-                    if (StringUtils.isBlank(updatedUser.getUsername())) {
-                        updatedUser.setUsername(originalUser.getUsername());
-                    }
-
-                    // update password if and only if password is really changed
-                    User user = userDAO.authFind(key);
-                    if (StringUtils.isBlank(updatedUser.getPassword())
-                            || ENCRYPTOR.verify(updatedUser.getPassword(),
-                                    user.getCipherAlgorithm(), user.getPassword())) {
-
-                        updatedUser.setPassword(null);
-                    }
-
-                    updatedUser.setSecurityQuestion(originalUser.getSecurityQuestion());
-
-                    if (!mappingManager.hasMustChangePassword(provision)) {
-                        updatedUser.setMustChangePassword(originalUser.isMustChangePassword());
-                    }
-
-                    anyPatch = (T) AnyOperations.diff(updatedUser, originalUser, true);
-                    break;
-
-                case GROUP:
-                    GroupTO originalGroup = (GroupTO) original;
-                    GroupTO updatedGroup = (GroupTO) updated;
-
-                    if (StringUtils.isBlank(updatedGroup.getName())) {
-                        updatedGroup.setName(originalGroup.getName());
-                    }
-                    updatedGroup.setUserOwner(originalGroup.getUserOwner());
-                    updatedGroup.setGroupOwner(originalGroup.getGroupOwner());
-                    updatedGroup.setUDynMembershipCond(originalGroup.getUDynMembershipCond());
-                    updatedGroup.getADynMembershipConds().putAll(originalGroup.getADynMembershipConds());
-                    updatedGroup.getTypeExtensions().addAll(originalGroup.getTypeExtensions());
-
-                    anyPatch = (T) AnyOperations.diff(updatedGroup, originalGroup, true);
-                    break;
-
-                case ANY_OBJECT:
-                    AnyObjectTO originalAnyObject = (AnyObjectTO) original;
-                    AnyObjectTO updatedAnyObject = (AnyObjectTO) updated;
-
-                    if (StringUtils.isBlank(updatedAnyObject.getName())) {
-                        updatedAnyObject.setName(originalAnyObject.getName());
-                    }
-
-                    anyPatch = (T) AnyOperations.diff(updatedAnyObject, originalAnyObject, true);
-                    break;
-
-                default:
-            }
+        switch (anyUtils.anyTypeKind()) {
+            case USER:
+                UserTO originalUser = (UserTO) original;
+                UserTO updatedUser = (UserTO) updated;
+
+                if (StringUtils.isBlank(updatedUser.getUsername())) {
+                    updatedUser.setUsername(originalUser.getUsername());
+                }
+
+                // update password if and only if password is really changed
+                User user = userDAO.authFind(key);
+                if (StringUtils.isBlank(updatedUser.getPassword())
+                        || ENCRYPTOR.verify(updatedUser.getPassword(),
+                                user.getCipherAlgorithm(), user.getPassword())) {
+
+                    updatedUser.setPassword(null);
+                }
+
+                updatedUser.setSecurityQuestion(originalUser.getSecurityQuestion());
+
+                if (!mappingManager.hasMustChangePassword(provision)) {
+                    updatedUser.setMustChangePassword(originalUser.isMustChangePassword());
+                }
+
+                anyPatch = (T) AnyOperations.diff(updatedUser, originalUser, true);
+                break;
+
+            case GROUP:
+                GroupTO originalGroup = (GroupTO) original;
+                GroupTO updatedGroup = (GroupTO) updated;
+
+                if (StringUtils.isBlank(updatedGroup.getName())) {
+                    updatedGroup.setName(originalGroup.getName());
+                }
+                updatedGroup.setUserOwner(originalGroup.getUserOwner());
+                updatedGroup.setGroupOwner(originalGroup.getGroupOwner());
+                updatedGroup.setUDynMembershipCond(originalGroup.getUDynMembershipCond());
+                updatedGroup.getADynMembershipConds().putAll(originalGroup.getADynMembershipConds());
+                updatedGroup.getTypeExtensions().addAll(originalGroup.getTypeExtensions());
+
+                anyPatch = (T) AnyOperations.diff(updatedGroup, originalGroup, true);
+                break;
+
+            case ANY_OBJECT:
+                AnyObjectTO originalAnyObject = (AnyObjectTO) original;
+                AnyObjectTO updatedAnyObject = (AnyObjectTO) updated;
+
+                if (StringUtils.isBlank(updatedAnyObject.getName())) {
+                    updatedAnyObject.setName(originalAnyObject.getName());
+                }
+
+                anyPatch = (T) AnyOperations.diff(updatedAnyObject, originalAnyObject, true);
+                break;
+
+            default:
         }
+
         // SYNCOPE-1343, remove null or empty values from the patch plain attributes
         if (anyPatch != null) {
             AnyOperations.cleanEmptyAttrs(updated, anyPatch);
diff --git a/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java b/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
index c97f93a..50bf5d9 100644
--- a/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
+++ b/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
@@ -346,7 +346,7 @@ public class CamelUserProvisioningManager extends AbstractCamelProvisioningManag
         Exception ex = (Exception) exchange.getProperty(Exchange.EXCEPTION_CAUGHT);
         if (ex != null) {
             LOG.error("Update of user {} failed, trying to pull its status anyway (if configured)",
-                    nullPriorityAsync, ex);
+                    userPatch.getKey(), ex);
 
             result.setStatus(ProvisioningReport.Status.FAILURE);
             result.setMessage("Update failed, trying to pull status anyway (if configured)\n" + ex.getMessage());
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
index f24eb79..e972305 100644
--- a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/init/OIDCClientLoader.java
@@ -41,15 +41,8 @@ public class OIDCClientLoader implements SyncopeLoader {
     public void load() {
         EntitlementsHolder.getInstance().init(OIDCClientEntitlement.values());
 
-        for (String domain : domainsHolder.getDomains().keySet()) {
-            AuthContextUtils.execWithAuthContext(domain, new AuthContextUtils.Executable<Void>() {
-
-                @Override
-                public Void exec() {
-                    return null;
-                }
-            });
-        }
+        domainsHolder.getDomains().forEach((domain, datasource) -> {
+            AuthContextUtils.execWithAuthContext(domain, () -> null);
+        });
     }
-
 }
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
index cf0004d..81b6390 100644
--- a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.tuple.Pair;
@@ -244,9 +245,10 @@ public class OIDCUserManager {
         }
 
         List<OIDCProviderActions> actions = getActions(op);
-        for (OIDCProviderActions action : actions) {
-            userTO = action.beforeCreate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.beforeCreate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         fill(op, responseTO, userTO);
 
@@ -260,9 +262,10 @@ public class OIDCUserManager {
         Pair<String, List<PropagationStatus>> created = provisioningManager.create(userTO, false, false);
         userTO = binder.getUserTO(created.getKey());
 
-        for (OIDCProviderActions action : actions) {
-            userTO = action.afterCreate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.afterCreate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         return userTO.getUsername();
     }
@@ -277,16 +280,18 @@ public class OIDCUserManager {
         UserPatch userPatch = AnyOperations.diff(userTO, original, true);
 
         List<OIDCProviderActions> actions = getActions(op);
-        for (OIDCProviderActions action : actions) {
-            userPatch = action.beforeUpdate(userPatch, responseTO);
-        }
+        userPatch = actions.stream().
+                map(action -> action.beforeUpdate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userPatch);
 
         Pair<UserPatch, List<PropagationStatus>> updated = provisioningManager.update(userPatch, false);
         userTO = binder.getUserTO(updated.getLeft().getKey());
 
-        for (OIDCProviderActions action : actions) {
-            userTO = action.afterUpdate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.afterUpdate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         return userTO.getUsername();
     }
diff --git a/ext/oidcclient/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/OIDCProviderActions.java b/ext/oidcclient/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/OIDCProviderActions.java
index 40ea6a8..4a641ad 100644
--- a/ext/oidcclient/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/OIDCProviderActions.java
+++ b/ext/oidcclient/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/OIDCProviderActions.java
@@ -18,18 +18,26 @@
  */
 package org.apache.syncope.core.provisioning.api;
 
+import java.util.function.Function;
 import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.OIDCLoginResponseTO;
 import org.apache.syncope.common.lib.to.UserTO;
 
 public interface OIDCProviderActions {
 
-    UserTO beforeCreate(UserTO input, OIDCLoginResponseTO loginResponse);
+    default Function<UserTO, UserTO> beforeCreate(OIDCLoginResponseTO loginResponse) {
+        return Function.identity();
+    }
 
-    UserTO afterCreate(UserTO input, OIDCLoginResponseTO loginResponse);
+    default Function<UserTO, UserTO> afterCreate(OIDCLoginResponseTO loginResponse) {
+        return Function.identity();
+    }
 
-    UserPatch beforeUpdate(UserPatch input, OIDCLoginResponseTO loginResponse);
-
-    UserTO afterUpdate(UserTO input, OIDCLoginResponseTO loginResponse);
+    default Function<UserPatch, UserPatch> beforeUpdate(OIDCLoginResponseTO loginResponse) {
+        return Function.identity();
+    }
 
+    default Function<UserTO, UserTO> afterUpdate(OIDCLoginResponseTO loginResponse) {
+        return Function.identity();
+    }
 }
diff --git a/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultOIDCProviderActions.java b/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultOIDCProviderActions.java
deleted file mode 100644
index e6b060c..0000000
--- a/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultOIDCProviderActions.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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;
-
-import org.apache.syncope.common.lib.patch.UserPatch;
-import org.apache.syncope.common.lib.to.OIDCLoginResponseTO;
-import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.core.provisioning.api.OIDCProviderActions;
-
-public class DefaultOIDCProviderActions implements OIDCProviderActions {
-
-    @Override
-    public UserTO beforeCreate(final UserTO input, final OIDCLoginResponseTO loginResponse) {
-        return input;
-    }
-
-    @Override
-    public UserTO afterCreate(final UserTO input, final OIDCLoginResponseTO loginResponse) {
-        return input;
-    }
-
-    @Override
-    public UserPatch beforeUpdate(final UserPatch input, final OIDCLoginResponseTO loginResponse) {
-        return input;
-    }
-
-    @Override
-    public UserTO afterUpdate(final UserTO input, final OIDCLoginResponseTO loginResponse) {
-        return input;
-    }
-
-}
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java
index 196d731..8c79b69 100644
--- a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/init/SAML2SPLoader.java
@@ -139,7 +139,7 @@ public class SAML2SPLoader implements SyncopeLoader {
             inited = false;
         }
 
-        domainsHolder.getDomains().keySet().forEach(domain -> {
+        domainsHolder.getDomains().forEach((domain, datasource) -> {
             AuthContextUtils.execWithAuthContext(domain, () -> {
                 idpDAO.findAll().forEach(idp -> {
                     try {
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
index 5f7d2b6..a555dc9 100644
--- a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.SerializationUtils;
 import org.apache.commons.lang3.tuple.Pair;
@@ -258,9 +259,10 @@ public class SAML2UserManager {
         }
 
         List<SAML2IdPActions> actions = getActions(idp);
-        for (SAML2IdPActions action : actions) {
-            userTO = action.beforeCreate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.beforeCreate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         fill(idp.getKey(), responseTO, userTO);
 
@@ -274,9 +276,10 @@ public class SAML2UserManager {
         Pair<String, List<PropagationStatus>> created = provisioningManager.create(userTO, false, false);
         userTO = binder.getUserTO(created.getKey());
 
-        for (SAML2IdPActions action : actions) {
-            userTO = action.afterCreate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.afterCreate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         return userTO.getUsername();
     }
@@ -291,16 +294,18 @@ public class SAML2UserManager {
         UserPatch userPatch = AnyOperations.diff(userTO, original, true);
 
         List<SAML2IdPActions> actions = getActions(idp);
-        for (SAML2IdPActions action : actions) {
-            userPatch = action.beforeUpdate(userPatch, responseTO);
-        }
+        userPatch = actions.stream().
+                map(action -> action.beforeUpdate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userPatch);
 
         Pair<UserPatch, List<PropagationStatus>> updated = provisioningManager.update(userPatch, false);
         userTO = binder.getUserTO(updated.getLeft().getKey());
 
-        for (SAML2IdPActions action : actions) {
-            userTO = action.afterUpdate(userTO, responseTO);
-        }
+        userTO = actions.stream().
+                map(action -> action.afterUpdate(responseTO)).
+                reduce(Function.identity(), Function::andThen).
+                apply(userTO);
 
         return userTO.getUsername();
     }
diff --git a/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java b/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java
index d8ec455..ef3bcb4 100644
--- a/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java
+++ b/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java
@@ -18,25 +18,26 @@
  */
 package org.apache.syncope.core.provisioning.api;
 
+import java.util.function.Function;
 import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
 import org.apache.syncope.common.lib.to.UserTO;
 
 public interface SAML2IdPActions {
 
-    default UserTO beforeCreate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    default Function<UserTO, UserTO> beforeCreate(SAML2LoginResponseTO loginResponse) {
+        return Function.identity();
     }
 
-    default UserTO afterCreate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    default Function<UserTO, UserTO> afterCreate(SAML2LoginResponseTO loginResponse) {
+        return Function.identity();
     }
 
-    default UserPatch beforeUpdate(UserPatch input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    default Function<UserPatch, UserPatch> beforeUpdate(SAML2LoginResponseTO loginResponse) {
+        return Function.identity();
     }
 
-    default UserTO afterUpdate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    default Function<UserTO, UserTO> afterUpdate(SAML2LoginResponseTO loginResponse) {
+        return Function.identity();
     }
 }
diff --git a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/DateParamConverterProvider.java b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/DateParamConverterProvider.java
new file mode 100644
index 0000000..123bde8
--- /dev/null
+++ b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/DateParamConverterProvider.java
@@ -0,0 +1,61 @@
+/*
+ * 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.fit.buildtools.cxf;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.Date;
+import javax.ws.rs.ext.ParamConverter;
+import javax.ws.rs.ext.ParamConverterProvider;
+import org.apache.commons.lang3.StringUtils;
+
+public class DateParamConverterProvider implements ParamConverterProvider {
+
+    private static class DateParamConverter implements ParamConverter<Date> {
+
+        @Override
+        public Date fromString(final String value) {
+            if (StringUtils.isBlank(value)) {
+                return null;
+            }
+            try {
+                return new Date(Long.valueOf(value));
+            } catch (final NumberFormatException e) {
+                throw new IllegalArgumentException("Unparsable date: " + value, e);
+            }
+        }
+
+        @Override
+        public String toString(final Date value) {
+            return value == null ? null : String.valueOf(value.getTime());
+        }
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> ParamConverter<T> getConverter(
+            final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
+
+        if (Date.class.equals(rawType)) {
+            return (ParamConverter<T>) new DateParamConverter();
+        }
+
+        return null;
+    }
+}
diff --git a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/User.java b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/User.java
index 18b3c7b..add6f94 100644
--- a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/User.java
+++ b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/User.java
@@ -25,6 +25,12 @@ public class User implements Serializable {
 
     private static final long serialVersionUID = -7906946710921162676L;
 
+    public enum Status {
+        ACTIVE,
+        INACTIVE;
+
+    }
+
     private UUID key;
 
     private String username;
@@ -37,6 +43,8 @@ public class User implements Serializable {
 
     private String email;
 
+    private Status status;
+
     public UUID getKey() {
         return key;
     }
@@ -85,4 +93,11 @@ public class User implements Serializable {
         this.email = email;
     }
 
+    public Status getStatus() {
+        return status;
+    }
+
+    public void setStatus(final Status status) {
+        this.status = status;
+    }
 }
diff --git a/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserMetadata.java
similarity index 52%
copy from ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java
copy to fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserMetadata.java
index d8ec455..d6591b4 100644
--- a/ext/saml2sp/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/SAML2IdPActions.java
+++ b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserMetadata.java
@@ -16,27 +16,42 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api;
+package org.apache.syncope.fit.buildtools.cxf;
 
-import org.apache.syncope.common.lib.patch.UserPatch;
-import org.apache.syncope.common.lib.to.SAML2LoginResponseTO;
-import org.apache.syncope.common.lib.to.UserTO;
+import java.io.Serializable;
+import java.util.Date;
 
-public interface SAML2IdPActions {
+public class UserMetadata implements Serializable {
 
-    default UserTO beforeCreate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    private static final long serialVersionUID = -5448360771141372951L;
+
+    private User user;
+
+    private Date lastChangeDate;
+
+    private boolean deleted;
+
+    public User getUser() {
+        return user;
+    }
+
+    public void setUser(final User user) {
+        this.user = user;
+    }
+
+    public Date getLastChangeDate() {
+        return lastChangeDate;
     }
 
-    default UserTO afterCreate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    public void setLastChangeDate(final Date lastChangeDate) {
+        this.lastChangeDate = lastChangeDate;
     }
 
-    default UserPatch beforeUpdate(UserPatch input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    public boolean isDeleted() {
+        return deleted;
     }
 
-    default UserTO afterUpdate(UserTO input, SAML2LoginResponseTO loginResponse) {
-        return input;
+    public void setDeleted(final boolean deleted) {
+        this.deleted = deleted;
     }
 }
diff --git a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserService.java b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserService.java
index 700ff9d..509a44b 100644
--- a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserService.java
+++ b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserService.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.fit.buildtools.cxf;
 
+import java.util.Date;
 import java.util.List;
 import java.util.UUID;
 import javax.ws.rs.Consumes;
@@ -40,6 +41,11 @@ public interface UserService {
     List<User> list();
 
     @GET
+    @Path("changelog")
+    @Produces({ MediaType.APPLICATION_JSON })
+    List<UserMetadata> changelog(@QueryParam("from") Date from);
+
+    @GET
     @Path("{key}")
     @Produces({ MediaType.APPLICATION_JSON })
     User read(@PathParam("key") UUID key);
diff --git a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserServiceImpl.java b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserServiceImpl.java
index 6b04515..321767f 100644
--- a/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserServiceImpl.java
+++ b/fit/build-tools/src/main/java/org/apache/syncope/fit/buildtools/cxf/UserServiceImpl.java
@@ -18,11 +18,14 @@
  */
 package org.apache.syncope.fit.buildtools.cxf;
 
-import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.ws.rs.ClientErrorException;
 import javax.ws.rs.ForbiddenException;
 import javax.ws.rs.NotFoundException;
@@ -34,23 +37,35 @@ import org.springframework.stereotype.Service;
 @Service
 public class UserServiceImpl implements UserService {
 
-    private static final Map<UUID, User> USERS = new HashMap<UUID, User>();
+    private static final Map<UUID, UserMetadata> USERS = new HashMap<>();
 
     @Context
     private UriInfo uriInfo;
 
     @Override
     public List<User> list() {
-        return new ArrayList<>(USERS.values());
+        return USERS.values().stream().
+                filter(meta -> !meta.isDeleted()).
+                map(UserMetadata::getUser).
+                collect(Collectors.toList());
+    }
+
+    @Override
+    public List<UserMetadata> changelog(final Date from) {
+        Stream<UserMetadata> users = USERS.values().stream();
+        if (from != null) {
+            users = users.filter(meta -> meta.getLastChangeDate().after(from));
+        }
+        return users.collect(Collectors.toList());
     }
 
     @Override
     public User read(final UUID key) {
-        User user = USERS.get(key);
-        if (user == null) {
+        UserMetadata meta = USERS.get(key);
+        if (meta == null || meta.isDeleted()) {
             throw new NotFoundException(key.toString());
         }
-        return user;
+        return meta.getUser();
     }
 
     @Override
@@ -58,66 +73,81 @@ public class UserServiceImpl implements UserService {
         if (user.getKey() == null) {
             user.setKey(UUID.randomUUID());
         }
-        if (USERS.containsKey(user.getKey())) {
+        if (user.getStatus() == null) {
+            user.setStatus(User.Status.ACTIVE);
+        }
+
+        UserMetadata meta = USERS.get(user.getKey());
+        if (meta != null && !meta.isDeleted()) {
             throw new ClientErrorException("User already exists: " + user.getKey(), Response.Status.CONFLICT);
         }
-        USERS.put(user.getKey(), user);
+
+        meta = new UserMetadata();
+        meta.setLastChangeDate(new Date());
+        meta.setUser(user);
+        USERS.put(user.getKey(), meta);
 
         return Response.created(uriInfo.getAbsolutePathBuilder().path(user.getKey().toString()).build()).build();
     }
 
     @Override
     public void update(final UUID key, final User updatedUser) {
-        if (!USERS.containsKey(key)) {
-            throw new NotFoundException(updatedUser.getKey().toString());
+        UserMetadata meta = USERS.get(key);
+        if (meta == null || meta.isDeleted()) {
+            throw new NotFoundException(key.toString());
         }
-        User user = USERS.get(key);
+
         if (updatedUser.getUsername() != null) {
-            user.setUsername(updatedUser.getUsername());
+            meta.getUser().setUsername(updatedUser.getUsername());
         }
         if (updatedUser.getPassword() != null) {
-            user.setPassword(updatedUser.getPassword());
+            meta.getUser().setPassword(updatedUser.getPassword());
         }
         if (updatedUser.getFirstName() != null) {
-            user.setFirstName(updatedUser.getFirstName());
+            meta.getUser().setFirstName(updatedUser.getFirstName());
         }
         if (updatedUser.getSurname() != null) {
-            user.setSurname(updatedUser.getSurname());
+            meta.getUser().setSurname(updatedUser.getSurname());
         }
         if (updatedUser.getEmail() != null) {
-            user.setEmail(updatedUser.getEmail());
+            meta.getUser().setEmail(updatedUser.getEmail());
+        }
+        if (updatedUser.getStatus() != null) {
+            meta.getUser().setStatus(updatedUser.getStatus());
         }
+
+        meta.setLastChangeDate(new Date());
     }
 
     @Override
     public void delete(final UUID key) {
-        if (!USERS.containsKey(key)) {
+        UserMetadata meta = USERS.get(key);
+        if (meta == null || meta.isDeleted()) {
             throw new NotFoundException(key.toString());
         }
-        USERS.remove(key);
+
+        meta.setDeleted(true);
+        meta.setLastChangeDate(new Date());
     }
 
     @Override
     public User authenticate(final String username, final String password) {
-        User user = null;
-        for (User entry : USERS.values()) {
-            if (username.equals(entry.getUsername())) {
-                user = entry;
-            }
-        }
-        if (user == null) {
+        Optional<User> user = USERS.values().stream().
+                filter(meta -> !meta.isDeleted() && username.equals(meta.getUser().getUsername())).
+                findFirst().map(UserMetadata::getUser);
+
+        if (!user.isPresent()) {
             throw new NotFoundException(username);
         }
-        if (!password.equals(user.getPassword())) {
+        if (!password.equals(user.get().getPassword())) {
             throw new ForbiddenException();
         }
 
-        return user;
+        return user.get();
     }
 
     @Override
     public void clear() {
         USERS.clear();
     }
-
 }
diff --git a/fit/build-tools/src/main/resources/cxfContext.xml b/fit/build-tools/src/main/resources/cxfContext.xml
index 7384e55..cb0f122 100644
--- a/fit/build-tools/src/main/resources/cxfContext.xml
+++ b/fit/build-tools/src/main/resources/cxfContext.xml
@@ -59,13 +59,17 @@ under the License.
     
     <property name="customizer" ref="openApiCustomizer"/>
   </bean>
-  
+
   <bean id="jsonProvider" class="com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider"/>
+
+  <bean id="dateParameterConverterProvider" class="org.apache.syncope.fit.buildtools.cxf.DateParamConverterProvider"/>
+
   <jaxrs:server id="restProvisioning" address="/rest"
                 basePackages="org.apache.syncope.fit.buildtools.cxf" 
                 staticSubresourceResolution="true">
     <jaxrs:providers>
       <ref bean="jsonProvider"/>
+      <ref bean="dateParameterConverterProvider"/>
     </jaxrs:providers>
     <jaxrs:features>
       <ref bean="openapiFeature"/>
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
index a7ee903..9cc0a85 100644
--- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/ITImplementationLookup.java
@@ -132,6 +132,7 @@ public class ITImplementationLookup implements ImplementationLookup {
         {
             put(DummyPullCorrelationRuleConf.class, DummyPullCorrelationRule.class);
             put(DefaultPullCorrelationRuleConf.class, DefaultPullCorrelationRule.class);
+            put(LinkedAccountSamplePullCorrelationRuleConf.class, LinkedAccountSamplePullCorrelationRule.class);
         }
     };
 
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRule.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRule.java
new file mode 100644
index 0000000..fed103d
--- /dev/null
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRule.java
@@ -0,0 +1,78 @@
+/*
+ * 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.fit.core.reference;
+
+import java.util.Optional;
+import org.apache.syncope.core.persistence.api.dao.PullCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.PullCorrelationRuleConfClass;
+import org.apache.syncope.core.persistence.api.dao.PullMatch;
+import org.apache.syncope.core.persistence.api.dao.search.AttributeCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.identityconnectors.framework.common.objects.SyncDelta;
+import org.springframework.util.CollectionUtils;
+
+@PullCorrelationRuleConfClass(LinkedAccountSamplePullCorrelationRuleConf.class)
+public class LinkedAccountSamplePullCorrelationRule implements PullCorrelationRule {
+
+    public static final String VIVALDI_KEY = "b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee";
+
+    @Override
+    public SearchCond getSearchCond(final SyncDelta syncDelta, final Provision provision) {
+        AttributeCond cond = new AttributeCond();
+
+        Attribute email = syncDelta.getObject().getAttributeByName("email");
+        if (email != null && !CollectionUtils.isEmpty(email.getValue())) {
+            cond.setSchema("email");
+            cond.setType(AttributeCond.Type.EQ);
+            cond.setExpression(email.getValue().get(0).toString());
+        } else {
+            cond.setSchema("");
+        }
+
+        return SearchCond.getLeafCond(cond);
+    }
+
+    @Override
+    public PullMatch matching(final Any<?> any, final SyncDelta syncDelta, final Provision provision) {
+        // if match with internal user vivaldi was found but firstName is different, update linked account
+        // instead of updating user
+        Attribute firstName = syncDelta.getObject().getAttributeByName("firstName");
+        if (VIVALDI_KEY.equals(any.getKey())
+                && firstName != null && !CollectionUtils.isEmpty(firstName.getValue())
+                && !"Antonio".equals(firstName.getValue().get(0).toString())) {
+
+            return new PullMatch.Builder().
+                    linkingUserKey(VIVALDI_KEY).
+                    matchTarget(PullMatch.MatchTarget.LINKED_ACCOUNT).build();
+        }
+
+        return PullCorrelationRule.super.matching(any, syncDelta, provision);
+    }
+
+    @Override
+    public Optional<PullMatch> unmatching(final SyncDelta syncDelta, final Provision provision) {
+        // if no match with internal user was found, link account to vivaldi instead of creating new user
+        return Optional.of(new PullMatch.Builder().
+                linkingUserKey(VIVALDI_KEY).
+                matchTarget(PullMatch.MatchTarget.LINKED_ACCOUNT).build());
+    }
+}
diff --git a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRuleConf.java
similarity index 60%
copy from client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
copy to fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRuleConf.java
index df22aa2..4981c88 100644
--- a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyLogicActions.groovy
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/LinkedAccountSamplePullCorrelationRuleConf.java
@@ -16,23 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import groovy.transform.CompileStatic
-import org.apache.syncope.common.lib.patch.AnyPatch
-import org.apache.syncope.common.lib.patch.AttrPatch
-import org.apache.syncope.common.lib.to.AnyTO
-import org.apache.syncope.common.lib.to.AttrTO
-import org.apache.syncope.core.provisioning.api.LogicActions
+package org.apache.syncope.fit.core.reference;
 
-@CompileStatic
-class MyLogicActions implements LogicActions {
-  
-  @Override
-  <A extends AnyTO> A beforeCreate(final A input) {
-    return input;
-  }
+import org.apache.syncope.common.lib.policy.AbstractCorrelationRuleConf;
+import org.apache.syncope.common.lib.policy.PullCorrelationRuleConf;
+
+public class LinkedAccountSamplePullCorrelationRuleConf
+        extends AbstractCorrelationRuleConf implements PullCorrelationRuleConf {
+
+    private static final long serialVersionUID = -958386962492907926L;
 
-  @Override
-  <M extends AnyPatch> M beforeUpdate(final M input) {
-    return input;
-  }
 }
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 020db67..ca14447 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
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.fail;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
@@ -122,6 +123,8 @@ public abstract class AbstractITCase {
 
     protected static final Logger LOG = LoggerFactory.getLogger(AbstractITCase.class);
 
+    protected static final ObjectMapper MAPPER = new ObjectMapper();
+
     protected static final String ADMIN_UNAME = "admin";
 
     protected static final String ADMIN_PWD = "password";
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/BatchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/BatchITCase.java
index ac8a001..8763f29 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/BatchITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/BatchITCase.java
@@ -25,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -69,8 +68,6 @@ import org.junit.jupiter.api.Test;
 
 public class BatchITCase extends AbstractITCase {
 
-    private static final ObjectMapper MAPPER = new ObjectMapper();
-
     private String requestBody(final String boundary) throws JsonProcessingException, JAXBException {
         List<BatchRequestItem> reqItems = new ArrayList<>();
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
index c6c4311..2bb5c22 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/DynRealmITCase.java
@@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.MediaType;
@@ -63,8 +62,6 @@ import org.junit.jupiter.api.Test;
 
 public class DynRealmITCase extends AbstractITCase {
 
-    private static final ObjectMapper MAPPER = new ObjectMapper();
-
     @Test
     public void misc() {
         DynRealmTO dynRealm = null;
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
index c6476e3..b7b3320 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
@@ -18,42 +18,60 @@
  */
 package org.apache.syncope.fit.core;
 
-import static org.apache.syncope.fit.AbstractITCase.getObject;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 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 com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 import javax.naming.NamingException;
 import javax.naming.directory.Attributes;
 import javax.naming.ldap.LdapContext;
+import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.patch.LinkedAccountPatch;
 import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.policy.PullPolicyTO;
 import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.ExecTO;
+import org.apache.syncope.common.lib.to.ImplementationTO;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
+import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
+import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.common.lib.to.TaskTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ExecStatus;
+import org.apache.syncope.common.lib.types.ImplementationEngine;
+import org.apache.syncope.common.lib.types.ImplementationType;
 import org.apache.syncope.common.lib.types.MatchingRule;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.lib.types.PolicyType;
+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.lib.types.UnmatchingRule;
+import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.TaskService;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.fit.AbstractITCase;
+import org.apache.syncope.fit.core.reference.LinkedAccountSamplePullCorrelationRule;
+import org.apache.syncope.fit.core.reference.LinkedAccountSamplePullCorrelationRuleConf;
 import org.junit.jupiter.api.Test;
 
 public class LinkedAccountITCase extends AbstractITCase {
@@ -199,17 +217,18 @@ public class LinkedAccountITCase extends AbstractITCase {
         pwdCipherAlgo.getValues().set(0, "AES");
         configurationService.set(pwdCipherAlgo);
 
+        String userKey = null;
+        String connObjectKeyValue = UUID.randomUUID().toString();
         try {
             // 1. create user with linked account
             UserTO user = UserITCase.getSampleTO(
                     "linkedAccount" + RandomStringUtils.randomNumeric(5) + "@syncope.apache.org");
-            String connObjectKeyValue = UUID.randomUUID().toString();
 
             LinkedAccountTO account = new LinkedAccountTO.Builder(RESOURCE_NAME_REST, connObjectKeyValue).build();
             user.getLinkedAccounts().add(account);
 
             user = createUser(user).getEntity();
-            String userKey = user.getKey();
+            userKey = user.getKey();
             assertNotNull(userKey);
             assertNotEquals(userKey, connObjectKeyValue);
 
@@ -273,6 +292,216 @@ public class LinkedAccountITCase extends AbstractITCase {
             // restore initial cipher algorithm
             pwdCipherAlgo.getValues().set(0, origpwdCipherAlgo);
             configurationService.set(pwdCipherAlgo);
+
+            // delete user and accounts
+            if (userKey != null) {
+                WebClient.create(BUILD_TOOLS_ADDRESS + "/rest/users/" + connObjectKeyValue).delete();
+                WebClient.create(BUILD_TOOLS_ADDRESS + "/rest/users/" + userKey).delete();
+
+                userService.delete(userKey);
+            }
+        }
+    }
+
+    @Test
+    public void pull() {
+        // -----------------------------
+        // Add a custom policy with correlation rule
+        // -----------------------------
+        ResourceTO restResource = resourceService.read(RESOURCE_NAME_REST);
+        if (restResource.getPullPolicy() == null) {
+            ImplementationTO rule = null;
+            try {
+                rule = implementationService.read(
+                        ImplementationType.PULL_CORRELATION_RULE, "LinkedAccountSamplePullCorrelationRule");
+            } catch (SyncopeClientException e) {
+                if (e.getType().getResponseStatus() == Response.Status.NOT_FOUND) {
+                    rule = new ImplementationTO();
+                    rule.setKey("LinkedAccountSamplePullCorrelationRule");
+                    rule.setEngine(ImplementationEngine.JAVA);
+                    rule.setType(ImplementationType.PULL_CORRELATION_RULE);
+                    rule.setBody(POJOHelper.serialize(new LinkedAccountSamplePullCorrelationRuleConf()));
+                    Response response = implementationService.create(rule);
+                    rule = implementationService.read(
+                            rule.getType(), response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+                    assertNotNull(rule.getKey());
+                }
+            }
+            assertNotNull(rule);
+
+            PullPolicyTO policy = new PullPolicyTO();
+            policy.setDescription("Linked Account sample Pull policy");
+            policy.getCorrelationRules().put(AnyTypeKind.USER.name(), rule.getKey());
+            Response response = policyService.create(PolicyType.PULL, policy);
+            policy = policyService.read(PolicyType.PULL, response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+            assertNotNull(policy.getKey());
+
+            restResource.setPullPolicy(policy.getKey());
+            resourceService.update(restResource);
+        }
+
+        // -----------------------------
+        // -----------------------------
+        // Add a pull task
+        // -----------------------------
+        String pullTaskKey;
+
+        PagedResult<PullTaskTO> tasks = taskService.search(
+                new TaskQuery.Builder(TaskType.PULL).resource(RESOURCE_NAME_REST).build());
+        if (tasks.getTotalCount() > 0) {
+            pullTaskKey = tasks.getResult().get(0).getKey();
+        } else {
+            PullTaskTO task = new PullTaskTO();
+            task.setDestinationRealm(SyncopeConstants.ROOT_REALM);
+            task.setName("Linked Account Pull Task");
+            task.setActive(true);
+            task.setResource(RESOURCE_NAME_REST);
+            task.setPullMode(PullMode.INCREMENTAL);
+            task.setPerformCreate(true);
+            task.setPerformUpdate(true);
+            task.setPerformDelete(true);
+            task.setSyncStatus(true);
+
+            Response response = taskService.create(TaskType.PULL, task);
+            task = taskService.read(TaskType.PULL, response.getHeaderString(RESTHeaders.RESOURCE_KEY), false);
+            assertNotNull(task.getKey());
+            pullTaskKey = task.getKey();
+        }
+        assertNotNull(pullTaskKey);
+        // -----------------------------
+
+        // 1. create REST users
+        WebClient webClient = WebClient.create(BUILD_TOOLS_ADDRESS + "/rest/users").
+                accept(MediaType.APPLICATION_JSON_TYPE).type(MediaType.APPLICATION_JSON_TYPE);
+
+        ObjectNode user = MAPPER.createObjectNode();
+        user.put("username", "linkedaccount1");
+        user.put("password", "Password123");
+        user.put("firstName", "Pasquale");
+        user.put("surname", "Vivaldi");
+        user.put("email", "vivaldi@syncope.org");
+
+        Response response = webClient.post(user.toString());
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+        String user1Key = StringUtils.substringAfterLast(response.getHeaderString(HttpHeaders.LOCATION), "/");
+        assertNotNull(user1Key);
+
+        user = MAPPER.createObjectNode();
+        user.put("username", "vivaldi");
+        user.put("password", "Password123");
+        user.put("firstName", "Giovannino");
+        user.put("surname", "Vivaldi");
+        user.put("email", "vivaldi@syncope.org");
+
+        response = webClient.post(user.toString());
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+        String user2Key = StringUtils.substringAfterLast(response.getHeaderString(HttpHeaders.LOCATION), "/");
+        assertNotNull(user2Key);
+
+        user = MAPPER.createObjectNode();
+        user.put("username", "not.vivaldi");
+        user.put("password", "Password123");
+        user.put("email", "not.vivaldi@syncope.org");
+
+        response = webClient.post(user.toString());
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+        String user3Key = StringUtils.substringAfterLast(response.getHeaderString(HttpHeaders.LOCATION), "/");
+        assertNotNull(user3Key);
+
+        // 2. execute pull task and verify linked accounts were pulled
+        try {
+            List<LinkedAccountTO> accounts = userService.read("vivaldi").getLinkedAccounts();
+            assertTrue(accounts.isEmpty());
+
+            ExecTO exec = AbstractTaskITCase.execProvisioningTask(taskService, TaskType.PULL, pullTaskKey, 50, false);
+            assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(exec.getStatus()));
+
+            accounts = userService.read("vivaldi").getLinkedAccounts();
+            assertEquals(3, accounts.size());
+
+            Optional<LinkedAccountTO> firstAccount = accounts.stream().
+                    filter(account -> user1Key.equals(account.getConnObjectKeyValue())).
+                    findFirst();
+            assertTrue(firstAccount.isPresent());
+            assertFalse(firstAccount.get().isSuspended());
+            assertEquals(RESOURCE_NAME_REST, firstAccount.get().getResource());
+            assertEquals("linkedaccount1", firstAccount.get().getUsername());
+            assertEquals("Pasquale", firstAccount.get().getPlainAttr("firstname").get().getValues().get(0));
+
+            Optional<LinkedAccountTO> secondAccount = accounts.stream().
+                    filter(account -> user2Key.equals(account.getConnObjectKeyValue())).
+                    findFirst();
+            assertTrue(secondAccount.isPresent());
+            assertFalse(secondAccount.get().isSuspended());
+            assertEquals(RESOURCE_NAME_REST, secondAccount.get().getResource());
+            assertNull(secondAccount.get().getUsername());
+            assertEquals("Giovannino", secondAccount.get().getPlainAttr("firstname").get().getValues().get(0));
+
+            Optional<LinkedAccountTO> thirdAccount = accounts.stream().
+                    filter(account -> user3Key.equals(account.getConnObjectKeyValue())).
+                    filter(account -> "not.vivaldi".equals(account.getUsername())).
+                    findFirst();
+            assertTrue(thirdAccount.isPresent());
+            assertFalse(thirdAccount.get().isSuspended());
+            assertEquals(RESOURCE_NAME_REST, thirdAccount.get().getResource());
+            assertEquals("not.vivaldi", thirdAccount.get().getUsername());
+
+            // 3. update / remove REST users
+            response = webClient.path(user1Key).delete();
+            assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+
+            user = MAPPER.createObjectNode();
+            user.put("username", "linkedaccount2");
+            response = webClient.replacePath(user2Key).put(user.toString());
+            assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+
+            user = MAPPER.createObjectNode();
+            user.put("status", "INACTIVE");
+            response = webClient.replacePath(user3Key).put(user.toString());
+            assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
+
+            // 4. execute pull task again and verify linked accounts were pulled
+            exec = AbstractTaskITCase.execProvisioningTask(taskService, TaskType.PULL, pullTaskKey, 50, false);
+            assertEquals(ExecStatus.SUCCESS, ExecStatus.valueOf(exec.getStatus()));
+
+            accounts = userService.read("vivaldi").getLinkedAccounts();
+            assertEquals(2, accounts.size());
+
+            firstAccount = accounts.stream().
+                    filter(account -> user1Key.equals(account.getConnObjectKeyValue())).
+                    findFirst();
+            assertFalse(firstAccount.isPresent());
+
+            secondAccount = accounts.stream().
+                    filter(account -> user2Key.equals(account.getConnObjectKeyValue())).
+                    findFirst();
+            assertTrue(secondAccount.isPresent());
+            assertFalse(secondAccount.get().isSuspended());
+            assertEquals(user2Key, secondAccount.get().getConnObjectKeyValue());
+            assertEquals("linkedaccount2", secondAccount.get().getUsername());
+
+            thirdAccount = accounts.stream().
+                    filter(account -> "not.vivaldi".equals(account.getUsername())).
+                    findFirst();
+            assertTrue(thirdAccount.isPresent());
+            assertTrue(thirdAccount.get().isSuspended());
+            assertEquals(user3Key, thirdAccount.get().getConnObjectKeyValue());
+        } finally {
+            // clean up
+            UserPatch patch = new UserPatch();
+            patch.setKey(LinkedAccountSamplePullCorrelationRule.VIVALDI_KEY);
+            patch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                    operation(PatchOperation.DELETE).
+                    linkedAccountTO(new LinkedAccountTO.Builder(RESOURCE_NAME_REST, user2Key).build()).
+                    build());
+            patch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                    operation(PatchOperation.DELETE).
+                    linkedAccountTO(new LinkedAccountTO.Builder(RESOURCE_NAME_REST, user3Key).build()).
+                    build());
+            userService.update(patch);
+
+            webClient.replacePath(user2Key).delete();
+            webClient.replacePath(user3Key).delete();
         }
     }
 }
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OpenAPIITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OpenAPIITCase.java
index d2141f7..75ca075 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OpenAPIITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/OpenAPIITCase.java
@@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.io.InputStream;
 import javax.ws.rs.core.MediaType;
@@ -41,7 +40,7 @@ public class OpenAPIITCase extends AbstractITCase {
         Response response = webClient.get();
         assumeTrue(response.getStatus() == 200);
 
-        JsonNode tree = new ObjectMapper().readTree((InputStream) response.getEntity());
+        JsonNode tree = MAPPER.readTree((InputStream) response.getEntity());
         assertNotNull(tree);
 
         JsonNode info = tree.get("info");
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 98b2841..5b499e0 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
@@ -88,7 +88,6 @@ import org.apache.syncope.common.rest.api.beans.RemediationQuery;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.ConnectorService;
 import org.apache.syncope.common.rest.api.service.TaskService;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.provisioning.java.pushpull.DBPasswordPullActions;
 import org.apache.syncope.core.provisioning.java.pushpull.LDAPPasswordPullActions;
 import org.apache.syncope.core.spring.security.Encryptor;
@@ -487,26 +486,33 @@ public class PullTaskITCase extends AbstractTaskITCase {
         ProvisionTO provision = resource.getProvision("PRINTER").get();
         assertNotNull(provision);
 
+        ImplementationTO transformer = null;
+        try {
+            transformer = implementationService.read(
+                    ImplementationType.ITEM_TRANSFORMER, "PrefixItemTransformer");
+        } catch (SyncopeClientException e) {
+            if (e.getType().getResponseStatus() == Response.Status.NOT_FOUND) {
+                transformer = new ImplementationTO();
+                transformer.setKey("PrefixItemTransformer");
+                transformer.setEngine(ImplementationEngine.GROOVY);
+                transformer.setType(ImplementationType.ITEM_TRANSFORMER);
+                transformer.setBody(IOUtils.toString(
+                        getClass().getResourceAsStream("/PrefixItemTransformer.groovy"), StandardCharsets.UTF_8));
+                Response response = implementationService.create(transformer);
+                transformer = implementationService.read(
+                        transformer.getType(), response.getHeaderString(RESTHeaders.RESOURCE_KEY));
+                assertNotNull(transformer.getKey());
+            }
+        }
+        assertNotNull(transformer);
+
         ItemTO mappingItem = provision.getMapping().getItems().stream().
                 filter(object -> "location".equals(object.getIntAttrName())).findFirst().get();
         assertNotNull(mappingItem);
-
-        final String prefix = "PREFIX_";
-
-        ImplementationTO transformer = new ImplementationTO();
-        transformer.setKey("PrefixItemTransformer");
-        transformer.setEngine(ImplementationEngine.GROOVY);
-        transformer.setType(ImplementationType.ITEM_TRANSFORMER);
-        transformer.setBody(IOUtils.toString(
-                getClass().getResourceAsStream("/PrefixItemTransformer.groovy"), StandardCharsets.UTF_8));
-        Response response = implementationService.create(transformer);
-        transformer = implementationService.read(
-                transformer.getType(), response.getHeaderString(RESTHeaders.RESOURCE_KEY));
-        assertNotNull(transformer);
-
         mappingItem.getTransformers().clear();
         mappingItem.getTransformers().add(transformer.getKey());
 
+        final String prefix = "PREFIX_";
         try {
             resourceService.update(resource);
             resourceService.removeSyncToken(resource.getKey(), provision.getAnyType());
diff --git a/fit/core-reference/src/test/resources/DoubleValueLogicActions.groovy b/fit/core-reference/src/test/resources/DoubleValueLogicActions.groovy
index a7bc87d..3557204 100644
--- a/fit/core-reference/src/test/resources/DoubleValueLogicActions.groovy
+++ b/fit/core-reference/src/test/resources/DoubleValueLogicActions.groovy
@@ -1,3 +1,4 @@
+
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -17,6 +18,7 @@
  * under the License.
  */
 import groovy.transform.CompileStatic
+import java.util.function.Function
 import org.apache.syncope.common.lib.patch.AnyPatch
 import org.apache.syncope.common.lib.patch.AttrPatch
 import org.apache.syncope.common.lib.to.AnyTO
@@ -32,42 +34,50 @@ class DoubleValueLogicActions implements LogicActions {
   private static final String NAME = "makeItDouble";
 
   @Override
-  <A extends AnyTO> A beforeCreate(final A input) {
-    for (AttrTO attr : input.getPlainAttrs()) {
-      if (NAME.equals(attr.getSchema())) {
-        List<String> values = new ArrayList<String>(attr.getValues().size());
-        for (String value : attr.getValues()) {
-          try {
-            values.add(String.valueOf(2 * Long.parseLong(value)));
-          } catch (NumberFormatException e) {
-            // ignore
+  <A extends AnyTO> Function<A, A> beforeCreate() {
+    Function function = { 
+      A input ->
+      for (AttrTO attr : input.getPlainAttrs()) {
+        if (NAME.equals(attr.getSchema())) {
+          List<String> values = new ArrayList<String>(attr.getValues().size());
+          for (String value : attr.getValues()) {
+            try {
+              values.add(String.valueOf(2 * Long.parseLong(value)));
+            } catch (NumberFormatException e) {
+              // ignore
+            }
           }
+          attr.getValues().clear();
+          attr.getValues().addAll(values);
         }
-        attr.getValues().clear();
-        attr.getValues().addAll(values);
       }
-    }
 
-    return input;
+      return input;        
+    }
+    return function;
   }
 
   @Override
-  <M extends AnyPatch> M beforeUpdate(final M input) {
-    for (AttrPatch patch : input.getPlainAttrs()) {
-      if (NAME.equals(patch.getAttrTO().getSchema())) {
-        List<String> values = new ArrayList<String>(patch.getAttrTO().getValues().size());
-        for (String value : patch.getAttrTO().getValues()) {
-          try {
-            values.add(String.valueOf(2 * Long.parseLong(value)));
-          } catch (NumberFormatException e) {
-            // ignore
+  <P extends AnyPatch> Function<P, P> beforeUpdate() {
+    Function function = { 
+      P input ->
+      for (AttrPatch patch : input.getPlainAttrs()) {
+        if (NAME.equals(patch.getAttrTO().getSchema())) {
+          List<String> values = new ArrayList<String>(patch.getAttrTO().getValues().size());
+          for (String value : patch.getAttrTO().getValues()) {
+            try {
+              values.add(String.valueOf(2 * Long.parseLong(value)));
+            } catch (NumberFormatException e) {
+              // ignore
+            }
           }
+          patch.getAttrTO().getValues().clear();
+          patch.getAttrTO().getValues().addAll(values);
         }
-        patch.getAttrTO().getValues().clear();
-        patch.getAttrTO().getValues().addAll(values);
       }
-    }
 
-    return input;
+      return input;
+    }
+    return function;
   }
 }
diff --git a/fit/core-reference/src/test/resources/rest/SearchScript.groovy b/fit/core-reference/src/test/resources/rest/SearchScript.groovy
index a6f5abe..0631654 100644
--- a/fit/core-reference/src/test/resources/rest/SearchScript.groovy
+++ b/fit/core-reference/src/test/resources/rest/SearchScript.groovy
@@ -54,11 +54,10 @@ def buildConnectorObject(node) {
   return [
     __UID__:node.get("key").textValue(), 
     __NAME__:node.get("key").textValue(),
+    __ENABLE__:node.get("status").textValue().equals("ACTIVE"),
+    __PASSWORD__:new GuardedString(node.get("password").textValue().toCharArray()),
     key:node.get("key").textValue(),
     username:node.get("username").textValue(),
-    password:node.has("password") && node.get("password").textValue() != null
-    ? new GuardedString(node.get("password").textValue().toCharArray()) 
-  : null,
     firstName:node.get("firstName").textValue(),
     surname:node.get("surname").textValue(),
     email:node.get("email").textValue()
diff --git a/fit/core-reference/src/test/resources/rest/SyncScript.groovy b/fit/core-reference/src/test/resources/rest/SyncScript.groovy
index 1676a01..6911614 100644
--- a/fit/core-reference/src/test/resources/rest/SyncScript.groovy
+++ b/fit/core-reference/src/test/resources/rest/SyncScript.groovy
@@ -54,9 +54,10 @@ def buildConnectorObject(node) {
   return [
     __UID__:node.get("key").textValue(), 
     __NAME__:node.get("key").textValue(),
+    __ENABLE__:node.get("status").textValue().equals("ACTIVE"),
+    __PASSWORD__:new GuardedString(node.get("password").textValue().toCharArray()),
     key:node.get("key").textValue(),
     username:node.get("username").textValue(),
-    password:new GuardedString(node.get("password").textValue().toCharArray()),
     firstName:node.get("firstName").textValue(),
     surname:node.get("surname").textValue(),
     email:node.get("email").textValue()
@@ -84,17 +85,40 @@ if (action.equalsIgnoreCase("GET_LATEST_SYNC_TOKEN")) {
 
   switch (objectClass) {
   case "__ACCOUNT__":
-    webClient.path("/users");
+    webClient.path("/users/changelog");      
+    if (token != null) {
+      webClient.query("from", token.toString());            
+    }
+
+    log.ok("Sending GET to {0}", webClient.getCurrentURI().toASCIIString());
+
     Response response = webClient.get();    
+
+    log.ok("CHANGELOG response: {0} {1}", response.getStatus(), response.getHeaders());
+
+    if (response.getStatus() != 200) {
+      throw new RuntimeException("Unexpected response from server: " 
+        + response.getStatus() + " " + response.getHeaders());
+    }
+    
     ArrayNode node = mapper.readTree(response.getEntity());
     
     for (i = 0; i < node.size(); i++) {
-      result.add([
-          operation:"CREATE_OR_UPDATE",
-          uid:node.get(i).get("key").textValue(),
-          token:new Date().getTime(),
-          attributes:buildConnectorObject(node.get(i))
-        ]);
+      if (node.get(i).get("deleted").booleanValue()) {
+        result.add([
+            operation:"DELETE",
+            uid:node.get(i).get("user").get("key").textValue(),
+            token:node.get(i).get("lastChangeDate").longValue(),
+            attributes:[]
+          ]);        
+      } else {
+        result.add([
+            operation:"CREATE_OR_UPDATE",
+            uid:node.get(i).get("user").get("key").textValue(),
+            token:node.get(i).get("lastChangeDate").longValue(),
+            attributes:buildConnectorObject(node.get(i).get("user"))
+          ]);
+      }
     }
     break;
   }